Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 17 additions & 10 deletions src/routes/likernft/book/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ import { getStripeClient } from '../../../util/stripe';
import { filterNFTBookListingInfo, filterNFTBookPricesInfo } from '../../../util/ValidationHelper';
import type { NFTBookListingInfo, NFTBookPrice } from '../../../types/book';
import { uploadImageBufferToCache } from '../../../util/fileupload';
import { convertUSDPriceToCurrency } from '../../../util/pricing';
import { BOOK_PRICE_OVERRIDE_CURRENCIES, getStripeCurrencyOptionsFromNFTBookPrice } from '../../../util/pricing';
import { normalizeClassIdParam } from '../../../middleware/likernft';

const router = Router();
Expand Down Expand Up @@ -315,6 +315,9 @@ router.put(['/:classId/price/:priceIndex', '/class/:classId/price/:priceIndex'],
...oldPriceInfo,
...formatPriceInfo(price),
};
if (!price.priceInDecimalByCurrency) {
delete newPriceInfo.priceInDecimalByCurrency;
Comment on lines +318 to +319
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think this is not crucial

}

if (oldPriceInfo.stripeProductId) {
const stripe = getStripeClient();
Expand All @@ -325,19 +328,23 @@ router.put(['/:classId/price/:priceIndex', '/class/:classId/price/:priceIndex'],
metadata,
});
if (oldPriceInfo.stripePriceId) {
if (oldPriceInfo.priceInDecimal !== newPriceInfo.priceInDecimal) {
const oldCurrencyOverride = oldPriceInfo.priceInDecimalByCurrency || {};
const newCurrencyOverride = newPriceInfo.priceInDecimalByCurrency || {};
const isCurrencyOverrideChanged = BOOK_PRICE_OVERRIDE_CURRENCIES.some(
(currency) => oldCurrencyOverride[currency] !== newCurrencyOverride[currency],
);
if (
oldPriceInfo.priceInDecimal !== newPriceInfo.priceInDecimal
|| isCurrencyOverrideChanged
) {
const newStripePrice = await stripe.prices.create({
product: oldPriceInfo.stripeProductId,
currency: 'usd',
unit_amount: price.priceInDecimal,
currency_options: {
twd: {
unit_amount: convertUSDPriceToCurrency(price.priceInDecimal / 100, 'twd') * 100,
},
hkd: {
unit_amount: convertUSDPriceToCurrency(price.priceInDecimal / 100, 'hkd') * 100,
},
},
currency_options: getStripeCurrencyOptionsFromNFTBookPrice(
price.priceInDecimal,
newPriceInfo.priceInDecimalByCurrency,
),
});
await stripe.products.update(
oldPriceInfo.stripeProductId,
Expand Down
10 changes: 10 additions & 0 deletions src/types/book.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,10 +89,19 @@ export interface BookPurchaseCommissionFiltered extends Omit<BookPurchaseCommiss
timestamp?: number;
}

// Per-currency price overrides, in that currency's minor units (e.g. cents),
// matching the convention of `priceInDecimal`. A missing currency falls back
// to the index-based ladder conversion.
export interface BookPriceInDecimalByCurrency {
hkd?: number;
twd?: number;
}

export interface NFTBookPrice {
name?: string | Record<string, string>;
description?: string | Record<string, string>;
priceInDecimal: number;
priceInDecimalByCurrency?: BookPriceInDecimalByCurrency;
isAllowCustomPrice?: boolean;
isTippingEnabled?: boolean;
isUnlisted?: boolean;
Expand All @@ -109,6 +118,7 @@ export interface NFTBookPrice {
export interface NFTBookPriceFiltered {
index: number;
price: number;
priceInDecimalByCurrency?: BookPriceInDecimalByCurrency;
name?: string | Record<string, string>;
description?: string | Record<string, string>;
stock: number;
Expand Down
2 changes: 2 additions & 0 deletions src/util/ValidationHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -538,6 +538,7 @@ export function filterNFTBookPricesInfo(
name,
description,
priceInDecimal,
priceInDecimalByCurrency,
isAllowCustomPrice,
isTippingEnabled,
isUnlisted,
Expand All @@ -563,6 +564,7 @@ export function filterNFTBookPricesInfo(
isTippingEnabled: !priceInDecimal || isTippingEnabled,
order: order ?? index,
};
if (priceInDecimalByCurrency) payload.priceInDecimalByCurrency = priceInDecimalByCurrency;
if (isOwner) {
payload.sold = pSold;
payload.stock = pStock;
Expand Down
4 changes: 4 additions & 0 deletions src/util/api/likernft/book/cart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1214,6 +1214,7 @@ export async function formatCartItemsWithInfo(items: CartItem[]) {
const priceData = prices[priceIndex];
const {
priceInDecimal: originalPriceInDecimal,
priceInDecimalByCurrency,
stock,
isAllowCustomPrice,
name: priceNameObj,
Expand All @@ -1236,6 +1237,7 @@ export async function formatCartItemsWithInfo(items: CartItem[]) {
info = {
stock,
isAllowCustomPrice,
priceInDecimalByCurrency,
name,
description,
images,
Expand All @@ -1258,6 +1260,7 @@ export async function formatCartItemsWithInfo(items: CartItem[]) {
} = info;
const {
isAllowCustomPrice,
priceInDecimalByCurrency,
originalPriceInDecimal,
stock,
images,
Expand Down Expand Up @@ -1296,6 +1299,7 @@ export async function formatCartItemsWithInfo(items: CartItem[]) {
...item,
priceName,
priceInDecimal,
priceInDecimalByCurrency,
customPriceDiffInDecimal,
stock,
isAllowCustomPrice,
Expand Down
19 changes: 9 additions & 10 deletions src/util/api/likernft/book/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import { getBook3NFTClassPageURL } from '../../../liker-land';
import { updateAirtablePublicationRecord } from '../../../airtable';
import { checkIsTrustedPublisher } from './user';
import type { NFTBookListingInfo, NFTBookPrice } from '../../../../types/book';
import { convertUSDPriceToCurrency } from '../../../pricing';
import { getStripeCurrencyOptionsFromNFTBookPrice } from '../../../pricing';

export function getAuthorNameFromMetadata(author: unknown): string {
if (typeof author === 'string') {
Expand Down Expand Up @@ -162,6 +162,7 @@ export function formatPriceInfo(price: NFTBookPrice): NFTBookPrice {
name: nameInput,
description: descriptionInput,
priceInDecimal,
priceInDecimalByCurrency,
isAllowCustomPrice = false,
stock,
isAutoDeliver = false,
Expand All @@ -174,7 +175,7 @@ export function formatPriceInfo(price: NFTBookPrice): NFTBookPrice {
if (nameInput) name[locale] = nameInput[locale];
if (descriptionInput) description[locale] = descriptionInput[locale];
});
return {
const formatted: NFTBookPrice = {
name,
description,
priceInDecimal,
Expand All @@ -184,6 +185,8 @@ export function formatPriceInfo(price: NFTBookPrice): NFTBookPrice {
isUnlisted,
autoMemo,
};
if (priceInDecimalByCurrency) formatted.priceInDecimalByCurrency = priceInDecimalByCurrency;
return formatted;
}

export async function createStripeProductFromNFTBookPrice(classId: string, priceIndex: number, {
Expand All @@ -210,14 +213,10 @@ export async function createStripeProductFromNFTBookPrice(classId: string, price
default_price_data: {
currency: 'usd',
unit_amount: price.priceInDecimal,
currency_options: {
twd: {
unit_amount: convertUSDPriceToCurrency(price.priceInDecimal / 100, 'twd') * 100,
},
hkd: {
unit_amount: convertUSDPriceToCurrency(price.priceInDecimal / 100, 'hkd') * 100,
},
},
currency_options: getStripeCurrencyOptionsFromNFTBookPrice(
price.priceInDecimal,
price.priceInDecimalByCurrency,
),
},
url: getBook3NFTClassPageURL({ classId, priceIndex }),
metadata,
Expand Down
15 changes: 8 additions & 7 deletions src/util/api/likernft/book/purchase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ import { CartItemWithInfo, TransactionFeeInfo } from './type';
import {
getClassCurrentTokenId, isEVMClassId, mintNFT, triggerNFTIndexerUpdate,
} from '../../../evm/nft';
import { convertUSDPriceToCurrency } from '../../../pricing';
import { getCurrencyPriceInDecimal } from '../../../pricing';
import { checkIsFromLikerLand, calculateItemPrices } from './price';

// Re-export pure functions for backward compatibility
Expand Down Expand Up @@ -694,10 +694,11 @@ export async function formatStripeCheckoutSession({
images: item.images,
metadata: productMetadata,
},
unit_amount: convertUSDPriceToCurrency(
item.originalPriceInDecimal / 100,
unit_amount: getCurrencyPriceInDecimal(
item.originalPriceInDecimal,
currencyWithDefault,
) * 100,
item.priceInDecimalByCurrency,
),
},
adjustable_quantity: {
enabled: false,
Expand All @@ -706,10 +707,10 @@ export async function formatStripeCheckoutSession({
});
}
if (item.customPriceDiffInDecimal) {
const convertedPriceDiffInDecimal = convertUSDPriceToCurrency(
item.customPriceDiffInDecimal / 100,
const convertedPriceDiffInDecimal = getCurrencyPriceInDecimal(
item.customPriceDiffInDecimal,
currencyWithDefault,
) * 100;
);
lineItems.push({
price_data: {
currency: currencyWithDefault,
Expand Down
11 changes: 11 additions & 0 deletions src/util/api/likernft/book/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,23 @@ import {
MIN_BOOK_PRICE_DECIMAL,
NFT_BOOK_TEXT_DEFAULT_LOCALE,
} from '../../../../constant';
import { BOOK_PRICE_OVERRIDE_CURRENCIES } from '../../../pricing';

const LocalizedTextMap = z.record(z.string(), z.string())
.refine(
(m) => typeof m[NFT_BOOK_TEXT_DEFAULT_LOCALE] === 'string',
{ message: `default locale "${NFT_BOOK_TEXT_DEFAULT_LOCALE}" is required` },
);

const PriceInDecimalByCurrencySchema = z.object(
Object.fromEntries(
BOOK_PRICE_OVERRIDE_CURRENCIES.map((currency) => [
currency,
z.number().int().min(0),
]),
),
).partial();

export const NFTBookPriceSchema = z.object({
priceInDecimal: z.number()
.int()
Expand All @@ -18,6 +28,7 @@ export const NFTBookPriceSchema = z.object({
(v) => v === 0 || v >= MIN_BOOK_PRICE_DECIMAL,
{ message: `priceInDecimal must be 0 or >= ${MIN_BOOK_PRICE_DECIMAL}` },
),
priceInDecimalByCurrency: PriceInDecimalByCurrencySchema.optional(),
stock: z.number().int().min(0),
name: LocalizedTextMap,
description: LocalizedTextMap,
Expand Down
3 changes: 3 additions & 0 deletions src/util/api/likernft/book/type.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { BookPriceInDecimalByCurrency } from '../../../../types/book';

export type CartItem = {
classId?: string
priceIndex?: number
Expand All @@ -12,6 +14,7 @@ export type CartItemWithInfo = CartItem & {
customPriceDiffInDecimal: number;
stock: number;
isAllowCustomPrice: boolean;
priceInDecimalByCurrency?: BookPriceInDecimalByCurrency;
name: string,
description: string,
images: string[],
Expand Down
31 changes: 31 additions & 0 deletions src/util/pricing.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { USD_PRICE_TIER_LIST, HKD_PRICE_TIER_LIST, TWD_PRICE_TIER_LIST } from '../constant/pricing';
import type { SupportedPlusCurrency } from '../constant';
import type { BookPriceInDecimalByCurrency } from '../types/book';

const MAX_USD = USD_PRICE_TIER_LIST[USD_PRICE_TIER_LIST.length - 1]!;
const MAX_HKD = HKD_PRICE_TIER_LIST[HKD_PRICE_TIER_LIST.length - 1]!;
Expand Down Expand Up @@ -30,6 +31,36 @@ export function convertUSDPriceToCurrency(price: number, currency: SupportedPlus
}
}

// USD is excluded by design: it is the stored `priceInDecimal` and the commission base.
export const BOOK_PRICE_OVERRIDE_CURRENCIES = ['hkd', 'twd'] as const;

export function getCurrencyPriceInDecimal(
usdPriceInDecimal: number,
currency: SupportedPlusCurrency,
priceInDecimalByCurrency?: BookPriceInDecimalByCurrency,
): number {
if (currency === 'usd') return usdPriceInDecimal;
const override = priceInDecimalByCurrency?.[currency];
if (typeof override === 'number') return override;
return convertUSDPriceToCurrency(usdPriceInDecimal / 100, currency) * 100;
}
Comment on lines +40 to +46

export function getStripeCurrencyOptionsFromNFTBookPrice(
usdPriceInDecimal: number,
priceInDecimalByCurrency?: BookPriceInDecimalByCurrency,
) {
return Object.fromEntries(
BOOK_PRICE_OVERRIDE_CURRENCIES.map((currency) => {
const unitAmount = getCurrencyPriceInDecimal(
usdPriceInDecimal,
currency,
priceInDecimalByCurrency,
);
return [currency, { unit_amount: unitAmount }];
}),
);
}

export function convertCurrencyToUSDPrice(price: number, currency: SupportedPlusCurrency): number {
switch (currency) {
case 'hkd': {
Expand Down