@umec/core 0.1.0-alpha.5 → 0.1.0-alpha.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/checkout.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ type CheckoutOffer = {
|
|
|
9
9
|
freeShipping: boolean;
|
|
10
10
|
customFields?: CheckoutCustomField[];
|
|
11
11
|
metadata?: Record<string, string>;
|
|
12
|
+
customerEmail?: string;
|
|
12
13
|
unitsRemaining?: Map<string, number>;
|
|
13
14
|
};
|
|
14
15
|
type CheckoutOfferUnavailable = {
|
|
@@ -22,12 +23,13 @@ type CheckoutHandlerOptions = {
|
|
|
22
23
|
shipping?: UmecConfig["shipping"];
|
|
23
24
|
customFields?: CheckoutCustomField[];
|
|
24
25
|
metadata?: Record<string, string>;
|
|
26
|
+
customerEmail?: string;
|
|
25
27
|
checkStock?: CheckStock;
|
|
26
28
|
checkOffer?: CheckOffer;
|
|
27
29
|
};
|
|
28
30
|
type CheckoutHandler = ({ request }: {
|
|
29
31
|
request: Request;
|
|
30
32
|
}) => Promise<Response>;
|
|
31
|
-
declare function createCheckoutHandler({ products, shipping, customFields: staticCustomFields, metadata: staticMetadata, checkStock, checkOffer, }: CheckoutHandlerOptions): CheckoutHandler;
|
|
33
|
+
declare function createCheckoutHandler({ products, shipping, customFields: staticCustomFields, metadata: staticMetadata, customerEmail: staticCustomerEmail, checkStock, checkOffer, }: CheckoutHandlerOptions): CheckoutHandler;
|
|
32
34
|
|
|
33
35
|
export { type CheckOffer, type CheckStock, type CheckoutCustomField, type CheckoutHandler, type CheckoutHandlerOptions, type CheckoutOffer, type CheckoutOfferUnavailable, type CheckoutProductsMap, createCheckoutHandler };
|
package/dist/checkout.js
CHANGED
|
@@ -44,6 +44,7 @@ function createCheckoutHandler({
|
|
|
44
44
|
shipping,
|
|
45
45
|
customFields: staticCustomFields = [],
|
|
46
46
|
metadata: staticMetadata = {},
|
|
47
|
+
customerEmail: staticCustomerEmail,
|
|
47
48
|
checkStock,
|
|
48
49
|
checkOffer
|
|
49
50
|
}) {
|
|
@@ -86,6 +87,7 @@ function createCheckoutHandler({
|
|
|
86
87
|
let freeShipping = false;
|
|
87
88
|
let offerCustomFields = [];
|
|
88
89
|
let offerMetadata = {};
|
|
90
|
+
let customerEmail = staticCustomerEmail;
|
|
89
91
|
if (typeof offerSlug === "string" && offerSlug.trim() !== "") {
|
|
90
92
|
if (!checkOffer) return jsonError("Offer not available", 503);
|
|
91
93
|
const offer = await checkOffer(offerSlug.trim());
|
|
@@ -104,6 +106,7 @@ function createCheckoutHandler({
|
|
|
104
106
|
freeShipping = offer.freeShipping;
|
|
105
107
|
offerCustomFields = offer.customFields ?? [];
|
|
106
108
|
offerMetadata = offer.metadata ?? {};
|
|
109
|
+
customerEmail = offer.customerEmail ?? staticCustomerEmail;
|
|
107
110
|
}
|
|
108
111
|
const shippingRate = shipping ? freeShipping ? { amount: 0, label: shipping.freeLabel } : calcShipping(checkoutItems, products, shipping) : void 0;
|
|
109
112
|
const stock = checkStock ? await checkStock(checkoutItems.map((item) => item.sku)) : /* @__PURE__ */ new Map();
|
|
@@ -142,6 +145,7 @@ function createCheckoutHandler({
|
|
|
142
145
|
items: JSON.stringify(checkoutItems),
|
|
143
146
|
...typeof offerSlug === "string" && offerSlug.trim() ? { offer_slug: offerSlug.trim() } : {}
|
|
144
147
|
},
|
|
148
|
+
...customerEmail ? { customer_email: customerEmail } : {},
|
|
145
149
|
phone_number_collection: { enabled: true },
|
|
146
150
|
custom_fields: checkoutCustomFields,
|
|
147
151
|
shipping_address_collection: { allowed_countries: ["JP"] },
|
|
@@ -185,4 +189,4 @@ function createCheckoutHandler({
|
|
|
185
189
|
export {
|
|
186
190
|
createCheckoutHandler
|
|
187
191
|
};
|
|
188
|
-
//# sourceMappingURL=chunk-
|
|
192
|
+
//# sourceMappingURL=chunk-2EBXJT3M.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/checkout.ts"],"sourcesContent":["import Stripe from \"stripe\";\nimport type { UmecConfig, UmecProduct } from \"./config.js\";\nimport { getServerEnv } from \"./env.js\";\n\nconst STRIPE_API_VERSION = \"2026-05-27.dahlia\";\nconst MAX_ITEMS = 10;\nconst MAX_QUANTITY = 20;\n\ntype CheckoutItem = {\n sku: string;\n quantity: number;\n};\n\nexport type CheckoutProductsMap = Record<string, UmecProduct | undefined>;\n\nexport type CheckoutCustomField = Stripe.Checkout.SessionCreateParams.CustomField;\n\nexport type CheckoutOffer = {\n priceOverrides: Record<string, number>;\n freeShipping: boolean;\n customFields?: CheckoutCustomField[];\n metadata?: Record<string, string>;\n customerEmail?: string;\n unitsRemaining?: Map<string, number>;\n};\n\nexport type CheckoutOfferUnavailable = {\n error: \"Offer not found\" | \"Offer expired\";\n status: 404 | 410;\n};\n\nexport type CheckStock = (skus: string[]) => Promise<Map<string, number>>;\nexport type CheckOffer = (offerSlug: string) => Promise<CheckoutOffer | CheckoutOfferUnavailable | null>;\n\nexport type CheckoutHandlerOptions = {\n products: CheckoutProductsMap;\n shipping?: UmecConfig[\"shipping\"];\n customFields?: CheckoutCustomField[];\n metadata?: Record<string, string>;\n customerEmail?: string;\n checkStock?: CheckStock;\n checkOffer?: CheckOffer;\n};\n\nexport type CheckoutHandler = ({ request }: { request: Request }) => Promise<Response>;\n\nfunction jsonError(error: string, status: number) {\n return new Response(JSON.stringify({ error }), {\n status,\n headers: { \"Content-Type\": \"application/json\" },\n });\n}\n\nfunction isCheckoutPayload(value: unknown): value is { items?: unknown; offerSlug?: unknown } {\n return typeof value === \"object\" && value !== null;\n}\n\nfunction isValidQuantity(value: unknown): value is number {\n return typeof value === \"number\" && Number.isSafeInteger(value) && value >= 1;\n}\n\nfunction isOfferUnavailable(value: CheckoutOffer | CheckoutOfferUnavailable): value is CheckoutOfferUnavailable {\n return \"error\" in value && \"status\" in value;\n}\n\nfunction omitReservedMetadata(metadata: Record<string, string>) {\n return Object.fromEntries(\n Object.entries(metadata).filter(([key]) => key !== \"items\" && key !== \"offer_slug\"),\n );\n}\n\nfunction calcShipping(\n items: CheckoutItem[],\n products: CheckoutProductsMap,\n shipping: NonNullable<UmecConfig[\"shipping\"]>,\n) {\n const totalUnits = items.reduce((sum, { sku, quantity }) => {\n return sum + (products[sku]?.shippingUnits ?? 1) * quantity;\n }, 0);\n\n const tier = shipping.tiers.find((shippingTier) => {\n return shippingTier.maxUnits === undefined || totalUnits <= shippingTier.maxUnits;\n }) ?? shipping.tiers[shipping.tiers.length - 1]!;\n\n return {\n amount: tier.amount,\n label: tier.label,\n };\n}\n\nexport function createCheckoutHandler({\n products,\n shipping,\n customFields: staticCustomFields = [],\n metadata: staticMetadata = {},\n customerEmail: staticCustomerEmail,\n checkStock,\n checkOffer,\n}: CheckoutHandlerOptions): CheckoutHandler {\n return async ({ request }) => {\n const { STRIPE_SECRET_KEY: stripeKey } = getServerEnv();\n if (!stripeKey) return jsonError(\"Stripe not configured\", 503);\n\n const stripe = new Stripe(stripeKey, {\n apiVersion: STRIPE_API_VERSION,\n });\n let payload: unknown;\n\n try {\n payload = await request.json();\n } catch {\n return jsonError(\"Invalid request body\", 400);\n }\n\n if (!isCheckoutPayload(payload)) return jsonError(\"Invalid request body\", 400);\n\n const { items } = payload;\n if (!Array.isArray(items) || items.length === 0) return jsonError(\"Invalid items\", 400);\n if (items.length > MAX_ITEMS) return jsonError(\"Invalid items\", 400);\n\n const rawItems: CheckoutItem[] = [];\n for (const item of items) {\n if (typeof item !== \"object\" || item === null) return jsonError(\"Invalid SKU\", 400);\n\n const { sku, quantity } = item as { sku?: unknown; quantity?: unknown };\n if (typeof sku !== \"string\" || !products[sku]) return jsonError(\"Invalid SKU\", 400);\n if (!isValidQuantity(quantity)) return jsonError(\"Invalid quantity\", 400);\n\n rawItems.push({ sku, quantity });\n }\n\n const quantityMap = new Map<string, number>();\n for (const { sku, quantity } of rawItems) {\n quantityMap.set(sku, (quantityMap.get(sku) ?? 0) + quantity);\n }\n\n for (const [, total] of quantityMap) {\n if (total > MAX_QUANTITY) return jsonError(\"Invalid quantity\", 400);\n }\n\n const checkoutItems: CheckoutItem[] = Array.from(quantityMap.entries()).map(\n ([sku, quantity]) => ({ sku, quantity }),\n );\n const { offerSlug } = payload as { items?: unknown; offerSlug?: unknown };\n let priceOverrides: Record<string, number> = {};\n let freeShipping = false;\n let offerCustomFields: CheckoutCustomField[] = [];\n let offerMetadata: Record<string, string> = {};\n let customerEmail = staticCustomerEmail;\n\n if (typeof offerSlug === \"string\" && offerSlug.trim() !== \"\") {\n if (!checkOffer) return jsonError(\"Offer not available\", 503);\n const offer = await checkOffer(offerSlug.trim());\n if (!offer) return jsonError(\"Offer not found\", 404);\n if (isOfferUnavailable(offer)) return jsonError(offer.error, offer.status);\n const offerUnits = offer.unitsRemaining;\n if (offerUnits && offerUnits.size > 0) {\n for (const { sku, quantity } of checkoutItems) {\n const remaining = offerUnits.get(sku);\n // TOCTOU race is acceptable for low-frequency sales. Move to Durable Objects if needed.\n if (remaining !== undefined && quantity > remaining) {\n return jsonError(`Offer unit limit reached: ${sku}`, 409);\n }\n }\n }\n priceOverrides = offer.priceOverrides;\n freeShipping = offer.freeShipping;\n offerCustomFields = offer.customFields ?? [];\n offerMetadata = offer.metadata ?? {};\n customerEmail = offer.customerEmail ?? staticCustomerEmail;\n }\n\n const shippingRate = shipping\n ? freeShipping\n ? { amount: 0, label: shipping.freeLabel }\n : calcShipping(checkoutItems, products, shipping)\n : undefined;\n const stock = checkStock ? await checkStock(checkoutItems.map((item) => item.sku)) : new Map<string, number>();\n for (const { sku, quantity } of checkoutItems) {\n const available = stock.get(sku);\n if (available !== undefined && quantity > available) {\n return jsonError(`Out of stock: ${sku}`, 409);\n }\n }\n\n const checkoutCustomFields: CheckoutCustomField[] = [\n ...staticCustomFields,\n ...offerCustomFields,\n {\n key: \"message\",\n label: { type: \"custom\", custom: \"メッセージ・ご要望(任意)\" },\n type: \"text\",\n optional: true,\n text: { maximum_length: 255 },\n },\n ];\n\n try {\n const sessionParams: Stripe.Checkout.SessionCreateParams = {\n ui_mode: \"embedded_page\",\n mode: \"payment\",\n line_items: checkoutItems.map(({ sku, quantity }) => ({\n price_data: {\n currency: \"jpy\",\n product_data: { name: products[sku]!.name },\n unit_amount: priceOverrides[sku] ?? products[sku]!.price,\n },\n quantity,\n })),\n metadata: {\n ...omitReservedMetadata(staticMetadata),\n ...omitReservedMetadata(offerMetadata),\n items: JSON.stringify(checkoutItems),\n ...(typeof offerSlug === \"string\" && offerSlug.trim() ? { offer_slug: offerSlug.trim() } : {}),\n },\n ...(customerEmail ? { customer_email: customerEmail } : {}),\n phone_number_collection: { enabled: true },\n custom_fields: checkoutCustomFields,\n shipping_address_collection: { allowed_countries: [\"JP\"] },\n ...(shippingRate\n ? {\n shipping_options: [\n {\n shipping_rate_data: {\n type: \"fixed_amount\",\n fixed_amount: { amount: shippingRate.amount, currency: \"jpy\" },\n display_name: shippingRate.label,\n delivery_estimate: {\n minimum: { unit: \"business_day\", value: 2 },\n maximum: { unit: \"business_day\", value: 5 },\n },\n },\n },\n ],\n }\n : {}),\n redirect_on_completion: \"never\",\n };\n\n const session = await stripe.checkout.sessions.create(sessionParams);\n\n return new Response(JSON.stringify({ clientSecret: session.client_secret }), {\n headers: { \"Content-Type\": \"application/json\" },\n });\n } catch (error) {\n const stripeError = error as {\n type?: string;\n code?: string;\n statusCode?: number;\n requestId?: string;\n };\n console.error(\"[checkout] stripe session create failed:\", {\n type: stripeError.type,\n code: stripeError.code,\n statusCode: stripeError.statusCode,\n requestId: stripeError.requestId,\n });\n\n if (stripeError.type === \"StripePermissionError\") {\n return jsonError(\"Stripe key does not have permission to create Checkout Sessions\", 503);\n }\n\n return jsonError(\"Checkout session could not be created\", 503);\n }\n };\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/checkout.ts"],"sourcesContent":["import Stripe from \"stripe\";\nimport type { UmecConfig, UmecProduct } from \"./config.js\";\nimport { getServerEnv } from \"./env.js\";\n\nconst STRIPE_API_VERSION = \"2026-05-27.dahlia\";\nconst MAX_ITEMS = 10;\nconst MAX_QUANTITY = 20;\n\ntype CheckoutItem = {\n sku: string;\n quantity: number;\n};\n\nexport type CheckoutProductsMap = Record<string, UmecProduct | undefined>;\n\nexport type CheckoutCustomField = Stripe.Checkout.SessionCreateParams.CustomField;\n\nexport type CheckoutOffer = {\n priceOverrides: Record<string, number>;\n freeShipping: boolean;\n customFields?: CheckoutCustomField[];\n metadata?: Record<string, string>;\n unitsRemaining?: Map<string, number>;\n};\n\nexport type CheckoutOfferUnavailable = {\n error: \"Offer not found\" | \"Offer expired\";\n status: 404 | 410;\n};\n\nexport type CheckStock = (skus: string[]) => Promise<Map<string, number>>;\nexport type CheckOffer = (offerSlug: string) => Promise<CheckoutOffer | CheckoutOfferUnavailable | null>;\n\nexport type CheckoutHandlerOptions = {\n products: CheckoutProductsMap;\n shipping?: UmecConfig[\"shipping\"];\n customFields?: CheckoutCustomField[];\n metadata?: Record<string, string>;\n checkStock?: CheckStock;\n checkOffer?: CheckOffer;\n};\n\nexport type CheckoutHandler = ({ request }: { request: Request }) => Promise<Response>;\n\nfunction jsonError(error: string, status: number) {\n return new Response(JSON.stringify({ error }), {\n status,\n headers: { \"Content-Type\": \"application/json\" },\n });\n}\n\nfunction isCheckoutPayload(value: unknown): value is { items?: unknown; offerSlug?: unknown } {\n return typeof value === \"object\" && value !== null;\n}\n\nfunction isValidQuantity(value: unknown): value is number {\n return typeof value === \"number\" && Number.isSafeInteger(value) && value >= 1;\n}\n\nfunction isOfferUnavailable(value: CheckoutOffer | CheckoutOfferUnavailable): value is CheckoutOfferUnavailable {\n return \"error\" in value && \"status\" in value;\n}\n\nfunction omitReservedMetadata(metadata: Record<string, string>) {\n return Object.fromEntries(\n Object.entries(metadata).filter(([key]) => key !== \"items\" && key !== \"offer_slug\"),\n );\n}\n\nfunction calcShipping(\n items: CheckoutItem[],\n products: CheckoutProductsMap,\n shipping: NonNullable<UmecConfig[\"shipping\"]>,\n) {\n const totalUnits = items.reduce((sum, { sku, quantity }) => {\n return sum + (products[sku]?.shippingUnits ?? 1) * quantity;\n }, 0);\n\n const tier = shipping.tiers.find((shippingTier) => {\n return shippingTier.maxUnits === undefined || totalUnits <= shippingTier.maxUnits;\n }) ?? shipping.tiers[shipping.tiers.length - 1]!;\n\n return {\n amount: tier.amount,\n label: tier.label,\n };\n}\n\nexport function createCheckoutHandler({\n products,\n shipping,\n customFields: staticCustomFields = [],\n metadata: staticMetadata = {},\n checkStock,\n checkOffer,\n}: CheckoutHandlerOptions): CheckoutHandler {\n return async ({ request }) => {\n const { STRIPE_SECRET_KEY: stripeKey } = getServerEnv();\n if (!stripeKey) return jsonError(\"Stripe not configured\", 503);\n\n const stripe = new Stripe(stripeKey, {\n apiVersion: STRIPE_API_VERSION,\n });\n let payload: unknown;\n\n try {\n payload = await request.json();\n } catch {\n return jsonError(\"Invalid request body\", 400);\n }\n\n if (!isCheckoutPayload(payload)) return jsonError(\"Invalid request body\", 400);\n\n const { items } = payload;\n if (!Array.isArray(items) || items.length === 0) return jsonError(\"Invalid items\", 400);\n if (items.length > MAX_ITEMS) return jsonError(\"Invalid items\", 400);\n\n const rawItems: CheckoutItem[] = [];\n for (const item of items) {\n if (typeof item !== \"object\" || item === null) return jsonError(\"Invalid SKU\", 400);\n\n const { sku, quantity } = item as { sku?: unknown; quantity?: unknown };\n if (typeof sku !== \"string\" || !products[sku]) return jsonError(\"Invalid SKU\", 400);\n if (!isValidQuantity(quantity)) return jsonError(\"Invalid quantity\", 400);\n\n rawItems.push({ sku, quantity });\n }\n\n const quantityMap = new Map<string, number>();\n for (const { sku, quantity } of rawItems) {\n quantityMap.set(sku, (quantityMap.get(sku) ?? 0) + quantity);\n }\n\n for (const [, total] of quantityMap) {\n if (total > MAX_QUANTITY) return jsonError(\"Invalid quantity\", 400);\n }\n\n const checkoutItems: CheckoutItem[] = Array.from(quantityMap.entries()).map(\n ([sku, quantity]) => ({ sku, quantity }),\n );\n const { offerSlug } = payload as { items?: unknown; offerSlug?: unknown };\n let priceOverrides: Record<string, number> = {};\n let freeShipping = false;\n let offerCustomFields: CheckoutCustomField[] = [];\n let offerMetadata: Record<string, string> = {};\n\n if (typeof offerSlug === \"string\" && offerSlug.trim() !== \"\") {\n if (!checkOffer) return jsonError(\"Offer not available\", 503);\n const offer = await checkOffer(offerSlug.trim());\n if (!offer) return jsonError(\"Offer not found\", 404);\n if (isOfferUnavailable(offer)) return jsonError(offer.error, offer.status);\n const offerUnits = offer.unitsRemaining;\n if (offerUnits && offerUnits.size > 0) {\n for (const { sku, quantity } of checkoutItems) {\n const remaining = offerUnits.get(sku);\n // TOCTOU race is acceptable for low-frequency sales. Move to Durable Objects if needed.\n if (remaining !== undefined && quantity > remaining) {\n return jsonError(`Offer unit limit reached: ${sku}`, 409);\n }\n }\n }\n priceOverrides = offer.priceOverrides;\n freeShipping = offer.freeShipping;\n offerCustomFields = offer.customFields ?? [];\n offerMetadata = offer.metadata ?? {};\n }\n\n const shippingRate = shipping\n ? freeShipping\n ? { amount: 0, label: shipping.freeLabel }\n : calcShipping(checkoutItems, products, shipping)\n : undefined;\n const stock = checkStock ? await checkStock(checkoutItems.map((item) => item.sku)) : new Map<string, number>();\n for (const { sku, quantity } of checkoutItems) {\n const available = stock.get(sku);\n if (available !== undefined && quantity > available) {\n return jsonError(`Out of stock: ${sku}`, 409);\n }\n }\n\n const checkoutCustomFields: CheckoutCustomField[] = [\n ...staticCustomFields,\n ...offerCustomFields,\n {\n key: \"message\",\n label: { type: \"custom\", custom: \"メッセージ・ご要望(任意)\" },\n type: \"text\",\n optional: true,\n text: { maximum_length: 255 },\n },\n ];\n\n try {\n const sessionParams: Stripe.Checkout.SessionCreateParams = {\n ui_mode: \"embedded_page\",\n mode: \"payment\",\n line_items: checkoutItems.map(({ sku, quantity }) => ({\n price_data: {\n currency: \"jpy\",\n product_data: { name: products[sku]!.name },\n unit_amount: priceOverrides[sku] ?? products[sku]!.price,\n },\n quantity,\n })),\n metadata: {\n ...omitReservedMetadata(staticMetadata),\n ...omitReservedMetadata(offerMetadata),\n items: JSON.stringify(checkoutItems),\n ...(typeof offerSlug === \"string\" && offerSlug.trim() ? { offer_slug: offerSlug.trim() } : {}),\n },\n phone_number_collection: { enabled: true },\n custom_fields: checkoutCustomFields,\n shipping_address_collection: { allowed_countries: [\"JP\"] },\n ...(shippingRate\n ? {\n shipping_options: [\n {\n shipping_rate_data: {\n type: \"fixed_amount\",\n fixed_amount: { amount: shippingRate.amount, currency: \"jpy\" },\n display_name: shippingRate.label,\n delivery_estimate: {\n minimum: { unit: \"business_day\", value: 2 },\n maximum: { unit: \"business_day\", value: 5 },\n },\n },\n },\n ],\n }\n : {}),\n redirect_on_completion: \"never\",\n };\n\n const session = await stripe.checkout.sessions.create(sessionParams);\n\n return new Response(JSON.stringify({ clientSecret: session.client_secret }), {\n headers: { \"Content-Type\": \"application/json\" },\n });\n } catch (error) {\n const stripeError = error as {\n type?: string;\n code?: string;\n statusCode?: number;\n requestId?: string;\n };\n console.error(\"[checkout] stripe session create failed:\", {\n type: stripeError.type,\n code: stripeError.code,\n statusCode: stripeError.statusCode,\n requestId: stripeError.requestId,\n });\n\n if (stripeError.type === \"StripePermissionError\") {\n return jsonError(\"Stripe key does not have permission to create Checkout Sessions\", 503);\n }\n\n return jsonError(\"Checkout session could not be created\", 503);\n }\n };\n}\n"],"mappings":"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","names":[]}
|