@umec/core 0.1.0-alpha.6 → 0.1.0-alpha.8

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.
@@ -49,19 +49,20 @@ function formatOrderLine(order, products, now = Date.now()) {
49
49
  const name = order.name?.trim() || "\u304A\u5BA2\u69D8";
50
50
  return `${name} | ${parseItems(order.items_json, products)} | ${formatYen(order.amount)} | ${elapsedDays}\u65E5`;
51
51
  }
52
- async function notifyUnshipped(orders, env, { alertFrom, alertTo, products = {} }) {
52
+ async function notifyUnshipped(orders, env, { alertFrom, alertTo, alertSubject, products = {} }) {
53
53
  if (orders.length === 0) return;
54
- if (!env.DISCORD_WEBHOOK_URL) throw new Error("DISCORD_WEBHOOK_URL is not configured");
55
54
  if (!env.RESEND_API_KEY) throw new Error("RESEND_API_KEY is not configured");
56
55
  const lines = orders.map((order) => formatOrderLine(order, products));
57
56
  const content = ["\u672A\u767A\u9001\u30A2\u30E9\u30FC\u30C8: pending \u306E\u6CE8\u6587\u304C\u3042\u308A\u307E\u3059\u3002", "", ...lines].join("\n");
58
- const discordResponse = await fetch(env.DISCORD_WEBHOOK_URL, {
59
- method: "POST",
60
- headers: { "Content-Type": "application/json" },
61
- body: JSON.stringify({ content, allowed_mentions: { parse: [] } })
62
- });
63
- if (!discordResponse.ok) {
64
- throw new Error(`Discord notification failed: ${discordResponse.status}`);
57
+ if (env.DISCORD_WEBHOOK_URL) {
58
+ const discordResponse = await fetch(env.DISCORD_WEBHOOK_URL, {
59
+ method: "POST",
60
+ headers: { "Content-Type": "application/json" },
61
+ body: JSON.stringify({ content, allowed_mentions: { parse: [] } })
62
+ });
63
+ if (!discordResponse.ok) {
64
+ throw new Error(`Discord notification failed: ${discordResponse.status}`);
65
+ }
65
66
  }
66
67
  const rows = lines.map((line) => {
67
68
  const [name, items, amount, elapsedDays] = line.split(" | ");
@@ -76,7 +77,7 @@ async function notifyUnshipped(orders, env, { alertFrom, alertTo, products = {}
76
77
  const { error } = await resend.emails.send({
77
78
  from: alertFrom,
78
79
  to: alertTo,
79
- subject: "\u3010emu\u3011\u672A\u767A\u9001\u30A2\u30E9\u30FC\u30C8",
80
+ subject: alertSubject,
80
81
  html: `<!DOCTYPE html>
81
82
  <html lang="ja">
82
83
  <head><meta charset="UTF-8"></head>
@@ -106,4 +107,4 @@ async function notifyUnshipped(orders, env, { alertFrom, alertTo, products = {}
106
107
  export {
107
108
  notifyUnshipped
108
109
  };
109
- //# sourceMappingURL=chunk-QRFFBMFN.js.map
110
+ //# sourceMappingURL=chunk-JWTXQAOL.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/notify.ts"],"sourcesContent":["import { Resend } from \"resend\";\n\nconst DAY_MS = 24 * 60 * 60 * 1000;\n\nexport type UnshippedOrder = {\n id: string;\n name: string | null;\n items_json: string | null;\n amount: number | null;\n created_at: number | string | null;\n};\n\nexport type NotifyEnv = {\n DISCORD_WEBHOOK_URL?: string;\n RESEND_API_KEY?: string;\n};\n\ntype CheckoutItem = {\n sku?: unknown;\n quantity?: unknown;\n};\n\nexport type NotifyProduct = {\n name: string;\n};\n\nexport type NotifyProductsMap = Record<string, NotifyProduct | undefined>;\n\nexport type NotifyUnshippedOptions = {\n alertFrom: string;\n alertTo: string;\n alertSubject: string;\n products?: NotifyProductsMap;\n};\n\nfunction formatYen(value: number | null) {\n if (value == null) return \"-\";\n return new Intl.NumberFormat(\"ja-JP\", {\n style: \"currency\",\n currency: \"JPY\",\n maximumFractionDigits: 0,\n }).format(value);\n}\n\nfunction parseTimestamp(value: number | string | null) {\n if (value == null || value === \"\") return null;\n const timestamp = typeof value === \"string\" ? Number(value) : value;\n return Number.isFinite(timestamp) ? timestamp : null;\n}\n\nfunction parseItems(itemsJson: string | null, products: NotifyProductsMap) {\n if (!itemsJson) return \"-\";\n\n try {\n const parsed: unknown = JSON.parse(itemsJson);\n if (!Array.isArray(parsed) || parsed.length === 0) return \"-\";\n\n return parsed\n .map((item) => {\n if (typeof item !== \"object\" || item === null) return null;\n const { sku, quantity } = item as CheckoutItem;\n const productSku = typeof sku === \"string\" ? sku : \"\";\n const productName = productSku && products[productSku] ? products[productSku].name : productSku || \"商品\";\n const itemQuantity = typeof quantity === \"number\" && Number.isFinite(quantity) ? quantity : 0;\n return `${productName} x ${itemQuantity}`;\n })\n .filter((item): item is string => Boolean(item))\n .join(\", \");\n } catch {\n return \"-\";\n }\n}\n\nfunction escapeHtml(value: string) {\n return value\n .replaceAll(\"&\", \"&amp;\")\n .replaceAll(\"<\", \"&lt;\")\n .replaceAll(\">\", \"&gt;\")\n .replaceAll('\"', \"&quot;\")\n .replaceAll(\"'\", \"&#39;\");\n}\n\nfunction notificationErrorSuffix(error: unknown) {\n if (typeof error !== \"object\" || error === null) return \"\";\n const value = error as { name?: unknown; statusCode?: unknown };\n const details = [\n typeof value.name === \"string\" ? `name=${value.name}` : null,\n typeof value.statusCode === \"number\" ? `statusCode=${value.statusCode}` : null,\n ].filter((detail): detail is string => detail !== null);\n\n return details.length > 0 ? ` (${details.join(\", \")})` : \"\";\n}\n\nfunction formatOrderLine(order: UnshippedOrder, products: NotifyProductsMap, now = Date.now()) {\n const createdAt = parseTimestamp(order.created_at);\n const elapsedDays = createdAt == null ? \"-\" : String(Math.floor((now - createdAt) / DAY_MS));\n const name = order.name?.trim() || \"お客様\";\n return `${name} | ${parseItems(order.items_json, products)} | ${formatYen(order.amount)} | ${elapsedDays}日`;\n}\n\nexport async function notifyUnshipped(\n orders: UnshippedOrder[],\n env: NotifyEnv,\n { alertFrom, alertTo, alertSubject, products = {} }: NotifyUnshippedOptions,\n) {\n if (orders.length === 0) return;\n if (!env.RESEND_API_KEY) throw new Error(\"RESEND_API_KEY is not configured\");\n\n const lines = orders.map((order) => formatOrderLine(order, products));\n const content = [\"未発送アラート: pending の注文があります。\", \"\", ...lines].join(\"\\n\");\n\n if (env.DISCORD_WEBHOOK_URL) {\n const discordResponse = await fetch(env.DISCORD_WEBHOOK_URL, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ content, allowed_mentions: { parse: [] } }),\n });\n if (!discordResponse.ok) {\n throw new Error(`Discord notification failed: ${discordResponse.status}`);\n }\n }\n\n const rows = lines\n .map((line) => {\n const [name, items, amount, elapsedDays] = line.split(\" | \");\n return `<tr>\n <td style=\"padding:8px;border-bottom:1px solid #eee;\">${escapeHtml(name ?? \"\")}</td>\n <td style=\"padding:8px;border-bottom:1px solid #eee;\">${escapeHtml(items ?? \"\")}</td>\n <td style=\"padding:8px;border-bottom:1px solid #eee;text-align:right;\">${escapeHtml(amount ?? \"\")}</td>\n <td style=\"padding:8px;border-bottom:1px solid #eee;text-align:right;\">${escapeHtml(elapsedDays ?? \"\")}</td>\n </tr>`;\n })\n .join(\"\");\n\n const resend = new Resend(env.RESEND_API_KEY);\n const { error } = await resend.emails.send({\n from: alertFrom,\n to: alertTo,\n subject: alertSubject,\n html: `<!DOCTYPE html>\n<html lang=\"ja\">\n<head><meta charset=\"UTF-8\"></head>\n<body style=\"font-family:sans-serif;color:#222;max-width:720px;margin:0 auto;padding:32px 16px;\">\n <h1 style=\"font-size:20px;margin:0 0 16px;\">未発送アラート</h1>\n <p style=\"font-size:14px;line-height:1.7;color:#555;\">pending のまま threshold を超えた注文があります。</p>\n <table style=\"width:100%;border-collapse:collapse;font-size:14px;\">\n <thead>\n <tr>\n <th style=\"padding:8px;border-bottom:2px solid #222;text-align:left;\">名前</th>\n <th style=\"padding:8px;border-bottom:2px solid #222;text-align:left;\">商品</th>\n <th style=\"padding:8px;border-bottom:2px solid #222;text-align:right;\">金額</th>\n <th style=\"padding:8px;border-bottom:2px solid #222;text-align:right;\">経過日数</th>\n </tr>\n </thead>\n <tbody>${rows}</tbody>\n </table>\n</body>\n</html>`,\n text: content,\n });\n if (error) {\n throw new Error(`Resend notification failed${notificationErrorSuffix(error)}`);\n }\n}\n"],"mappings":"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","names":[]}
@@ -52,6 +52,7 @@ function createWebhookHandler({
52
52
  recordRefund,
53
53
  recordExpiry,
54
54
  recordOrder,
55
+ recordEmailResult,
55
56
  restoreStock,
56
57
  decrementStock,
57
58
  incrementOfferSold,
@@ -177,6 +178,7 @@ function createWebhookHandler({
177
178
  shipping,
178
179
  customFields: cf
179
180
  });
181
+ let emailResult;
180
182
  try {
181
183
  const resend = new Resend(resendKey);
182
184
  const { error } = await resend.emails.send({
@@ -188,11 +190,26 @@ function createWebhookHandler({
188
190
  html
189
191
  });
190
192
  if (error) {
191
- console.error("[webhook] Resend error:", providerErrorDetails(error));
193
+ const details = providerErrorDetails(error);
194
+ console.error("[webhook] Resend error:", details);
195
+ emailResult = {
196
+ sessionId: session.id,
197
+ status: "failed",
198
+ ...details.message ? { errorMessage: details.message } : {}
199
+ };
200
+ } else {
201
+ emailResult = { sessionId: session.id, status: "sent" };
192
202
  }
193
203
  } catch (e) {
194
- console.error("[webhook] Resend exception:", providerErrorDetails(e));
204
+ const details = providerErrorDetails(e);
205
+ console.error("[webhook] Resend exception:", details);
206
+ emailResult = {
207
+ sessionId: session.id,
208
+ status: "failed",
209
+ ...details.message ? { errorMessage: details.message } : {}
210
+ };
195
211
  }
212
+ if (recordEmailResult) await recordEmailResult(emailResult);
196
213
  return new Response("OK", { status: 200 });
197
214
  };
198
215
  }
@@ -201,4 +218,4 @@ export {
201
218
  GET,
202
219
  createWebhookHandler
203
220
  };
204
- //# sourceMappingURL=chunk-MUH43YWF.js.map
221
+ //# sourceMappingURL=chunk-ZP7IGPJW.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/webhook.ts"],"sourcesContent":["import { Resend } from \"resend\";\nimport Stripe from \"stripe\";\nimport { getServerEnv } from \"./env.js\";\n\nconst STRIPE_API_VERSION = \"2026-05-27.dahlia\";\n\ntype ShippingDetails = NonNullable<\n NonNullable<Stripe.Checkout.Session[\"collected_information\"]>[\"shipping_details\"]\n>;\ntype SessionWithLegacyShipping = Stripe.Checkout.Session & {\n shipping_details?: ShippingDetails | null;\n};\nexport type WebhookItem = {\n sku: string;\n quantity: number;\n};\nexport type SupportedWebhookEvent =\n | \"checkout.session.completed\"\n | \"checkout.session.expired\"\n | \"charge.refunded\";\n\nexport type OrderEmailLineItem = {\n description: string | null;\n quantity: number | null;\n amount: number | null;\n};\n\nexport type OrderEmailData = {\n name: string;\n email: string;\n phone: string | null;\n items: OrderEmailLineItem[];\n totalAmount: number;\n shippingCostAmount: number | null;\n shipping: ShippingDetails | null | undefined;\n customFields: Record<string, string>;\n};\n\nexport type RenderOrderEmail = (order: OrderEmailData) => { subject: string; html: string };\n\nexport type WebhookOrder = {\n id: string;\n email: string;\n name: string | null;\n phone: string | null;\n items: WebhookItem[];\n amount: number;\n shippingCost: number | null;\n shippingAddress: ShippingDetails | null | undefined;\n paymentIntent: string | null;\n offerSlug: string | null;\n customFields: Record<string, string> | null;\n metadata: Record<string, string> | null;\n createdAt: number;\n};\n\nexport type WebhookRecordedOrder = {\n items: WebhookItem[];\n offerSlug: string | null;\n};\n\nexport type WebhookEmailResult = {\n sessionId: string;\n status: \"sent\" | \"failed\";\n errorMessage?: string;\n};\n\nexport type WebhookHandlerOptions = {\n claimEvent?: (eventId: string) => Promise<boolean>;\n recordRefund?: (paymentIntentId: string) => Promise<WebhookRecordedOrder | null>;\n recordExpiry?: (sessionId: string) => Promise<WebhookRecordedOrder | null>;\n recordOrder?: (order: WebhookOrder) => Promise<void>;\n recordEmailResult?: (result: WebhookEmailResult) => Promise<void>;\n restoreStock?: (items: WebhookItem[]) => Promise<void>;\n decrementStock?: (items: WebhookItem[]) => Promise<void>;\n incrementOfferSold?: (offerSlug: string, items: WebhookItem[]) => Promise<void>;\n decrementOfferSold?: (offerSlug: string, items: WebhookItem[]) => Promise<void>;\n renderOrderEmail?: RenderOrderEmail;\n orderEmailFrom?: string;\n orderEmailBcc?: string | string[];\n orderEmailReplyTo?: string;\n};\n\nexport type WebhookHandler = ({ request }: { request: Request }) => Promise<Response>;\n\nfunction parseCheckoutItems(items: string | null | undefined): WebhookItem[] {\n if (!items) return [];\n\n try {\n const parsed: unknown = JSON.parse(items);\n if (!Array.isArray(parsed)) return [];\n\n return parsed.filter((item): item is WebhookItem => {\n if (typeof item !== \"object\" || item === null) return false;\n const { sku, quantity } = item as { sku?: unknown; quantity?: unknown };\n return typeof sku === \"string\" && typeof quantity === \"number\" && Number.isSafeInteger(quantity) && quantity > 0;\n });\n } catch {\n return [];\n }\n}\n\nfunction isSupportedWebhookEvent(type: string): type is SupportedWebhookEvent {\n return (\n type === \"checkout.session.completed\" ||\n type === \"checkout.session.expired\" ||\n type === \"charge.refunded\"\n );\n}\n\nfunction getPaymentIntentId(paymentIntent: string | Stripe.PaymentIntent | null) {\n if (typeof paymentIntent === \"string\") return paymentIntent;\n return paymentIntent?.id ?? null;\n}\n\nfunction providerErrorDetails(error: unknown) {\n if (typeof error !== \"object\" || error === null) return { message: String(error) };\n const value = error as {\n name?: unknown;\n type?: unknown;\n code?: unknown;\n statusCode?: unknown;\n requestId?: unknown;\n message?: unknown;\n };\n\n return {\n name: typeof value.name === \"string\" ? value.name : undefined,\n type: typeof value.type === \"string\" ? value.type : undefined,\n code: typeof value.code === \"string\" ? value.code : undefined,\n statusCode: typeof value.statusCode === \"number\" ? value.statusCode : undefined,\n requestId: typeof value.requestId === \"string\" ? value.requestId : undefined,\n message: typeof value.message === \"string\" ? value.message : undefined,\n };\n}\n\nasync function restoreRecordedOrder(\n order: WebhookRecordedOrder | null,\n restoreStock?: (items: WebhookItem[]) => Promise<void>,\n decrementOfferSold?: (offerSlug: string, items: WebhookItem[]) => Promise<void>,\n) {\n const items = order?.items ?? [];\n if (items.length > 0 && restoreStock) await restoreStock(items);\n if (order?.offerSlug && decrementOfferSold) {\n await decrementOfferSold(order.offerSlug, items);\n }\n}\n\nexport const GET: WebhookHandler = async () => new Response(\"OK\", { status: 200 });\n\nexport function createWebhookHandler({\n claimEvent,\n recordRefund,\n recordExpiry,\n recordOrder,\n recordEmailResult,\n restoreStock,\n decrementStock,\n incrementOfferSold,\n decrementOfferSold,\n renderOrderEmail,\n orderEmailFrom,\n orderEmailBcc,\n orderEmailReplyTo,\n}: WebhookHandlerOptions = {}): WebhookHandler {\n return async ({ request }) => {\n console.log(\"[webhook] POST received, method:\", request.method);\n\n const {\n STRIPE_SECRET_KEY: stripeKey,\n STRIPE_WEBHOOK_SECRET: webhookSecret,\n RESEND_API_KEY: resendKey,\n } = getServerEnv();\n\n if (!stripeKey || !webhookSecret) {\n console.error(\"[webhook] Stripe env not configured\");\n return new Response(\"Server error\", { status: 500 });\n }\n\n const sig = request.headers.get(\"stripe-signature\");\n if (!sig) return new Response(\"Bad request\", { status: 400 });\n\n const payload = await request.text();\n\n const stripe = new Stripe(stripeKey, { apiVersion: STRIPE_API_VERSION });\n\n let event: Stripe.Event;\n try {\n event = await stripe.webhooks.constructEventAsync(payload, sig, webhookSecret);\n } catch {\n return new Response(\"Invalid signature\", { status: 400 });\n }\n\n if (!isSupportedWebhookEvent(event.type)) {\n return new Response(\"OK\", { status: 200 });\n }\n\n if (claimEvent) {\n const claim = await claimEvent(event.id);\n if (!claim) {\n console.log(\"[webhook] duplicate event skipped:\", event.id);\n return new Response(\"OK\", { status: 200 });\n }\n }\n\n if (event.type === \"charge.refunded\") {\n const charge = event.data.object as Stripe.Charge;\n const paymentIntent = getPaymentIntentId(charge.payment_intent);\n\n if (paymentIntent && recordRefund) {\n const order = await recordRefund(paymentIntent);\n await restoreRecordedOrder(order, restoreStock, decrementOfferSold);\n }\n\n return new Response(\"OK\", { status: 200 });\n }\n\n const session = event.data.object as Stripe.Checkout.Session;\n\n if (event.type === \"checkout.session.expired\") {\n if (recordExpiry) {\n const order = await recordExpiry(session.id);\n await restoreRecordedOrder(order, restoreStock, decrementOfferSold);\n }\n\n return new Response(\"OK\", { status: 200 });\n }\n\n const email = session.customer_details?.email;\n const customerName = session.customer_details?.name ?? null;\n const name = customerName ?? \"お客様\";\n const phone = session.customer_details?.phone ?? null;\n const totalAmount = session.amount_total ?? 0;\n const shippingCostAmount = session.shipping_cost?.amount_total ?? null;\n const paymentIntent = getPaymentIntentId(session.payment_intent);\n const checkoutItems = parseCheckoutItems(session.metadata?.items);\n const cf: Record<string, string> = {};\n for (const f of (session.custom_fields ?? []) as Array<{\n key: string;\n type: string;\n text?: { value?: string | null };\n dropdown?: { value?: string | null };\n }>) {\n const v = f.type === \"dropdown\" ? f.dropdown?.value : f.text?.value;\n if (typeof v === \"string\" && v.trim() !== \"\") cf[f.key] = v.trim();\n }\n const customFields = Object.keys(cf).length > 0 ? cf : null;\n const { items: _items, offer_slug: _offerSlug, ...externalMetadata } = session.metadata ?? {};\n const metadata = Object.keys(externalMetadata).length > 0 ? externalMetadata : null;\n\n const shipping =\n session.collected_information?.shipping_details ??\n (session as SessionWithLegacyShipping).shipping_details;\n\n const offerSlugFromMeta = session.metadata?.offer_slug ?? null;\n\n if (recordOrder) {\n await recordOrder({\n id: session.id,\n email: email ?? \"\",\n name: customerName,\n phone,\n items: checkoutItems,\n amount: totalAmount,\n shippingCost: shippingCostAmount,\n shippingAddress: shipping,\n paymentIntent,\n offerSlug: offerSlugFromMeta,\n customFields,\n metadata,\n createdAt: Date.now(),\n });\n if (checkoutItems.length > 0 && decrementStock) await decrementStock(checkoutItems);\n if (offerSlugFromMeta && checkoutItems.length > 0 && incrementOfferSold) {\n await incrementOfferSold(offerSlugFromMeta, checkoutItems);\n }\n }\n\n let lineItems: Stripe.LineItem[] = [];\n try {\n const result = await stripe.checkout.sessions.listLineItems(session.id, { limit: 100 });\n lineItems = result.data;\n } catch (e) {\n console.error(\"[webhook] listLineItems failed:\", providerErrorDetails(e));\n }\n\n if (!email) {\n console.error(\"[webhook] no customer email in session:\", session.id);\n return new Response(\"OK\", { status: 200 });\n }\n\n if (!resendKey || !renderOrderEmail || !orderEmailFrom) {\n if (!resendKey) console.error(\"[webhook] RESEND_API_KEY not set\");\n return new Response(\"OK\", { status: 200 });\n }\n\n const { subject, html } = renderOrderEmail({\n name,\n email,\n phone,\n items: lineItems.map((item) => ({\n description: item.description,\n quantity: item.quantity,\n amount: item.amount_total,\n })),\n totalAmount,\n shippingCostAmount,\n shipping,\n customFields: cf,\n });\n\n let emailResult: WebhookEmailResult;\n try {\n const resend = new Resend(resendKey);\n const { error } = await resend.emails.send({\n from: orderEmailFrom,\n to: email,\n ...(orderEmailBcc ? { bcc: orderEmailBcc } : {}),\n ...(orderEmailReplyTo ? { reply_to: orderEmailReplyTo } : {}),\n subject,\n html,\n });\n if (error) {\n const details = providerErrorDetails(error);\n console.error(\"[webhook] Resend error:\", details);\n emailResult = {\n sessionId: session.id,\n status: \"failed\",\n ...(details.message ? { errorMessage: details.message } : {}),\n };\n } else {\n emailResult = { sessionId: session.id, status: \"sent\" };\n }\n } catch (e) {\n const details = providerErrorDetails(e);\n console.error(\"[webhook] Resend exception:\", details);\n emailResult = {\n sessionId: session.id,\n status: \"failed\",\n ...(details.message ? { errorMessage: details.message } : {}),\n };\n }\n\n if (recordEmailResult) await recordEmailResult(emailResult);\n\n return new Response(\"OK\", { status: 200 });\n };\n}\n"],"mappings":"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","names":["paymentIntent"]}
package/dist/index.d.ts CHANGED
@@ -5,7 +5,7 @@ export { ServerEnv, getServerEnv } from './env.js';
5
5
  export { UmecLimitedPage, UmecMiddleware, UmecMiddlewareContext, UmecMiddlewareNext, UmecMiddlewareOptions, createUmecMiddleware } from './middleware.js';
6
6
  export { NotifyEnv, NotifyProduct, NotifyProductsMap, NotifyUnshippedOptions, UnshippedOrder, notifyUnshipped } from './notify.js';
7
7
  export { SubscribeHandler, SubscribeHandlerOptions, createSubscribeHandler } from './subscribe.js';
8
- export { OrderEmailData, OrderEmailLineItem, RenderOrderEmail, SupportedWebhookEvent, WebhookHandler, WebhookHandlerOptions, WebhookItem, WebhookOrder, WebhookRecordedOrder, createWebhookHandler, GET as webhookGET } from './webhook.js';
8
+ export { OrderEmailData, OrderEmailLineItem, RenderOrderEmail, SupportedWebhookEvent, WebhookEmailResult, WebhookHandler, WebhookHandlerOptions, WebhookItem, WebhookOrder, WebhookRecordedOrder, createWebhookHandler, GET as webhookGET } from './webhook.js';
9
9
  import 'jose';
10
10
  import 'stripe';
11
11
  import 'zod';
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  GET,
3
3
  createWebhookHandler
4
- } from "./chunk-MUH43YWF.js";
4
+ } from "./chunk-ZP7IGPJW.js";
5
5
  import {
6
6
  createAccessGuard
7
7
  } from "./chunk-2SNGXEIL.js";
@@ -18,7 +18,7 @@ import {
18
18
  } from "./chunk-4V4GYE7E.js";
19
19
  import {
20
20
  notifyUnshipped
21
- } from "./chunk-QRFFBMFN.js";
21
+ } from "./chunk-JWTXQAOL.js";
22
22
  import {
23
23
  createSubscribeHandler
24
24
  } from "./chunk-X2SNVVFW.js";
package/dist/notify.d.ts CHANGED
@@ -16,8 +16,9 @@ type NotifyProductsMap = Record<string, NotifyProduct | undefined>;
16
16
  type NotifyUnshippedOptions = {
17
17
  alertFrom: string;
18
18
  alertTo: string;
19
+ alertSubject: string;
19
20
  products?: NotifyProductsMap;
20
21
  };
21
- declare function notifyUnshipped(orders: UnshippedOrder[], env: NotifyEnv, { alertFrom, alertTo, products }: NotifyUnshippedOptions): Promise<void>;
22
+ declare function notifyUnshipped(orders: UnshippedOrder[], env: NotifyEnv, { alertFrom, alertTo, alertSubject, products }: NotifyUnshippedOptions): Promise<void>;
22
23
 
23
24
  export { type NotifyEnv, type NotifyProduct, type NotifyProductsMap, type NotifyUnshippedOptions, type UnshippedOrder, notifyUnshipped };
package/dist/notify.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  notifyUnshipped
3
- } from "./chunk-QRFFBMFN.js";
3
+ } from "./chunk-JWTXQAOL.js";
4
4
  export {
5
5
  notifyUnshipped
6
6
  };
package/dist/webhook.d.ts CHANGED
@@ -44,11 +44,17 @@ type WebhookRecordedOrder = {
44
44
  items: WebhookItem[];
45
45
  offerSlug: string | null;
46
46
  };
47
+ type WebhookEmailResult = {
48
+ sessionId: string;
49
+ status: "sent" | "failed";
50
+ errorMessage?: string;
51
+ };
47
52
  type WebhookHandlerOptions = {
48
53
  claimEvent?: (eventId: string) => Promise<boolean>;
49
54
  recordRefund?: (paymentIntentId: string) => Promise<WebhookRecordedOrder | null>;
50
55
  recordExpiry?: (sessionId: string) => Promise<WebhookRecordedOrder | null>;
51
56
  recordOrder?: (order: WebhookOrder) => Promise<void>;
57
+ recordEmailResult?: (result: WebhookEmailResult) => Promise<void>;
52
58
  restoreStock?: (items: WebhookItem[]) => Promise<void>;
53
59
  decrementStock?: (items: WebhookItem[]) => Promise<void>;
54
60
  incrementOfferSold?: (offerSlug: string, items: WebhookItem[]) => Promise<void>;
@@ -62,6 +68,6 @@ type WebhookHandler = ({ request }: {
62
68
  request: Request;
63
69
  }) => Promise<Response>;
64
70
  declare const GET: WebhookHandler;
65
- declare function createWebhookHandler({ claimEvent, recordRefund, recordExpiry, recordOrder, restoreStock, decrementStock, incrementOfferSold, decrementOfferSold, renderOrderEmail, orderEmailFrom, orderEmailBcc, orderEmailReplyTo, }?: WebhookHandlerOptions): WebhookHandler;
71
+ declare function createWebhookHandler({ claimEvent, recordRefund, recordExpiry, recordOrder, recordEmailResult, restoreStock, decrementStock, incrementOfferSold, decrementOfferSold, renderOrderEmail, orderEmailFrom, orderEmailBcc, orderEmailReplyTo, }?: WebhookHandlerOptions): WebhookHandler;
66
72
 
67
- export { GET, type OrderEmailData, type OrderEmailLineItem, type RenderOrderEmail, type SupportedWebhookEvent, type WebhookHandler, type WebhookHandlerOptions, type WebhookItem, type WebhookOrder, type WebhookRecordedOrder, createWebhookHandler };
73
+ export { GET, type OrderEmailData, type OrderEmailLineItem, type RenderOrderEmail, type SupportedWebhookEvent, type WebhookEmailResult, type WebhookHandler, type WebhookHandlerOptions, type WebhookItem, type WebhookOrder, type WebhookRecordedOrder, createWebhookHandler };
package/dist/webhook.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  GET,
3
3
  createWebhookHandler
4
- } from "./chunk-MUH43YWF.js";
4
+ } from "./chunk-ZP7IGPJW.js";
5
5
  import "./chunk-7TJANK6S.js";
6
6
  export {
7
7
  GET,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umec/core",
3
- "version": "0.1.0-alpha.6",
3
+ "version": "0.1.0-alpha.8",
4
4
  "description": "Headless EC logic for umec — config schema and shared handlers",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/webhook.ts"],"sourcesContent":["import { Resend } from \"resend\";\nimport Stripe from \"stripe\";\nimport { getServerEnv } from \"./env.js\";\n\nconst STRIPE_API_VERSION = \"2026-05-27.dahlia\";\n\ntype ShippingDetails = NonNullable<\n NonNullable<Stripe.Checkout.Session[\"collected_information\"]>[\"shipping_details\"]\n>;\ntype SessionWithLegacyShipping = Stripe.Checkout.Session & {\n shipping_details?: ShippingDetails | null;\n};\nexport type WebhookItem = {\n sku: string;\n quantity: number;\n};\nexport type SupportedWebhookEvent =\n | \"checkout.session.completed\"\n | \"checkout.session.expired\"\n | \"charge.refunded\";\n\nexport type OrderEmailLineItem = {\n description: string | null;\n quantity: number | null;\n amount: number | null;\n};\n\nexport type OrderEmailData = {\n name: string;\n email: string;\n phone: string | null;\n items: OrderEmailLineItem[];\n totalAmount: number;\n shippingCostAmount: number | null;\n shipping: ShippingDetails | null | undefined;\n customFields: Record<string, string>;\n};\n\nexport type RenderOrderEmail = (order: OrderEmailData) => { subject: string; html: string };\n\nexport type WebhookOrder = {\n id: string;\n email: string;\n name: string | null;\n phone: string | null;\n items: WebhookItem[];\n amount: number;\n shippingCost: number | null;\n shippingAddress: ShippingDetails | null | undefined;\n paymentIntent: string | null;\n offerSlug: string | null;\n customFields: Record<string, string> | null;\n metadata: Record<string, string> | null;\n createdAt: number;\n};\n\nexport type WebhookRecordedOrder = {\n items: WebhookItem[];\n offerSlug: string | null;\n};\n\nexport type WebhookHandlerOptions = {\n claimEvent?: (eventId: string) => Promise<boolean>;\n recordRefund?: (paymentIntentId: string) => Promise<WebhookRecordedOrder | null>;\n recordExpiry?: (sessionId: string) => Promise<WebhookRecordedOrder | null>;\n recordOrder?: (order: WebhookOrder) => Promise<void>;\n restoreStock?: (items: WebhookItem[]) => Promise<void>;\n decrementStock?: (items: WebhookItem[]) => Promise<void>;\n incrementOfferSold?: (offerSlug: string, items: WebhookItem[]) => Promise<void>;\n decrementOfferSold?: (offerSlug: string, items: WebhookItem[]) => Promise<void>;\n renderOrderEmail?: RenderOrderEmail;\n orderEmailFrom?: string;\n orderEmailBcc?: string | string[];\n orderEmailReplyTo?: string;\n};\n\nexport type WebhookHandler = ({ request }: { request: Request }) => Promise<Response>;\n\nfunction parseCheckoutItems(items: string | null | undefined): WebhookItem[] {\n if (!items) return [];\n\n try {\n const parsed: unknown = JSON.parse(items);\n if (!Array.isArray(parsed)) return [];\n\n return parsed.filter((item): item is WebhookItem => {\n if (typeof item !== \"object\" || item === null) return false;\n const { sku, quantity } = item as { sku?: unknown; quantity?: unknown };\n return typeof sku === \"string\" && typeof quantity === \"number\" && Number.isSafeInteger(quantity) && quantity > 0;\n });\n } catch {\n return [];\n }\n}\n\nfunction isSupportedWebhookEvent(type: string): type is SupportedWebhookEvent {\n return (\n type === \"checkout.session.completed\" ||\n type === \"checkout.session.expired\" ||\n type === \"charge.refunded\"\n );\n}\n\nfunction getPaymentIntentId(paymentIntent: string | Stripe.PaymentIntent | null) {\n if (typeof paymentIntent === \"string\") return paymentIntent;\n return paymentIntent?.id ?? null;\n}\n\nfunction providerErrorDetails(error: unknown) {\n if (typeof error !== \"object\" || error === null) return { message: String(error) };\n const value = error as {\n name?: unknown;\n type?: unknown;\n code?: unknown;\n statusCode?: unknown;\n requestId?: unknown;\n message?: unknown;\n };\n\n return {\n name: typeof value.name === \"string\" ? value.name : undefined,\n type: typeof value.type === \"string\" ? value.type : undefined,\n code: typeof value.code === \"string\" ? value.code : undefined,\n statusCode: typeof value.statusCode === \"number\" ? value.statusCode : undefined,\n requestId: typeof value.requestId === \"string\" ? value.requestId : undefined,\n message: typeof value.message === \"string\" ? value.message : undefined,\n };\n}\n\nasync function restoreRecordedOrder(\n order: WebhookRecordedOrder | null,\n restoreStock?: (items: WebhookItem[]) => Promise<void>,\n decrementOfferSold?: (offerSlug: string, items: WebhookItem[]) => Promise<void>,\n) {\n const items = order?.items ?? [];\n if (items.length > 0 && restoreStock) await restoreStock(items);\n if (order?.offerSlug && decrementOfferSold) {\n await decrementOfferSold(order.offerSlug, items);\n }\n}\n\nexport const GET: WebhookHandler = async () => new Response(\"OK\", { status: 200 });\n\nexport function createWebhookHandler({\n claimEvent,\n recordRefund,\n recordExpiry,\n recordOrder,\n restoreStock,\n decrementStock,\n incrementOfferSold,\n decrementOfferSold,\n renderOrderEmail,\n orderEmailFrom,\n orderEmailBcc,\n orderEmailReplyTo,\n}: WebhookHandlerOptions = {}): WebhookHandler {\n return async ({ request }) => {\n console.log(\"[webhook] POST received, method:\", request.method);\n\n const {\n STRIPE_SECRET_KEY: stripeKey,\n STRIPE_WEBHOOK_SECRET: webhookSecret,\n RESEND_API_KEY: resendKey,\n } = getServerEnv();\n\n if (!stripeKey || !webhookSecret) {\n console.error(\"[webhook] Stripe env not configured\");\n return new Response(\"Server error\", { status: 500 });\n }\n\n const sig = request.headers.get(\"stripe-signature\");\n if (!sig) return new Response(\"Bad request\", { status: 400 });\n\n const payload = await request.text();\n\n const stripe = new Stripe(stripeKey, { apiVersion: STRIPE_API_VERSION });\n\n let event: Stripe.Event;\n try {\n event = await stripe.webhooks.constructEventAsync(payload, sig, webhookSecret);\n } catch {\n return new Response(\"Invalid signature\", { status: 400 });\n }\n\n if (!isSupportedWebhookEvent(event.type)) {\n return new Response(\"OK\", { status: 200 });\n }\n\n if (claimEvent) {\n const claim = await claimEvent(event.id);\n if (!claim) {\n console.log(\"[webhook] duplicate event skipped:\", event.id);\n return new Response(\"OK\", { status: 200 });\n }\n }\n\n if (event.type === \"charge.refunded\") {\n const charge = event.data.object as Stripe.Charge;\n const paymentIntent = getPaymentIntentId(charge.payment_intent);\n\n if (paymentIntent && recordRefund) {\n const order = await recordRefund(paymentIntent);\n await restoreRecordedOrder(order, restoreStock, decrementOfferSold);\n }\n\n return new Response(\"OK\", { status: 200 });\n }\n\n const session = event.data.object as Stripe.Checkout.Session;\n\n if (event.type === \"checkout.session.expired\") {\n if (recordExpiry) {\n const order = await recordExpiry(session.id);\n await restoreRecordedOrder(order, restoreStock, decrementOfferSold);\n }\n\n return new Response(\"OK\", { status: 200 });\n }\n\n const email = session.customer_details?.email;\n const customerName = session.customer_details?.name ?? null;\n const name = customerName ?? \"お客様\";\n const phone = session.customer_details?.phone ?? null;\n const totalAmount = session.amount_total ?? 0;\n const shippingCostAmount = session.shipping_cost?.amount_total ?? null;\n const paymentIntent = getPaymentIntentId(session.payment_intent);\n const checkoutItems = parseCheckoutItems(session.metadata?.items);\n const cf: Record<string, string> = {};\n for (const f of (session.custom_fields ?? []) as Array<{\n key: string;\n type: string;\n text?: { value?: string | null };\n dropdown?: { value?: string | null };\n }>) {\n const v = f.type === \"dropdown\" ? f.dropdown?.value : f.text?.value;\n if (typeof v === \"string\" && v.trim() !== \"\") cf[f.key] = v.trim();\n }\n const customFields = Object.keys(cf).length > 0 ? cf : null;\n const { items: _items, offer_slug: _offerSlug, ...externalMetadata } = session.metadata ?? {};\n const metadata = Object.keys(externalMetadata).length > 0 ? externalMetadata : null;\n\n const shipping =\n session.collected_information?.shipping_details ??\n (session as SessionWithLegacyShipping).shipping_details;\n\n const offerSlugFromMeta = session.metadata?.offer_slug ?? null;\n\n if (recordOrder) {\n await recordOrder({\n id: session.id,\n email: email ?? \"\",\n name: customerName,\n phone,\n items: checkoutItems,\n amount: totalAmount,\n shippingCost: shippingCostAmount,\n shippingAddress: shipping,\n paymentIntent,\n offerSlug: offerSlugFromMeta,\n customFields,\n metadata,\n createdAt: Date.now(),\n });\n if (checkoutItems.length > 0 && decrementStock) await decrementStock(checkoutItems);\n if (offerSlugFromMeta && checkoutItems.length > 0 && incrementOfferSold) {\n await incrementOfferSold(offerSlugFromMeta, checkoutItems);\n }\n }\n\n let lineItems: Stripe.LineItem[] = [];\n try {\n const result = await stripe.checkout.sessions.listLineItems(session.id, { limit: 100 });\n lineItems = result.data;\n } catch (e) {\n console.error(\"[webhook] listLineItems failed:\", providerErrorDetails(e));\n }\n\n if (!email) {\n console.error(\"[webhook] no customer email in session:\", session.id);\n return new Response(\"OK\", { status: 200 });\n }\n\n if (!resendKey || !renderOrderEmail || !orderEmailFrom) {\n if (!resendKey) console.error(\"[webhook] RESEND_API_KEY not set\");\n return new Response(\"OK\", { status: 200 });\n }\n\n const { subject, html } = renderOrderEmail({\n name,\n email,\n phone,\n items: lineItems.map((item) => ({\n description: item.description,\n quantity: item.quantity,\n amount: item.amount_total,\n })),\n totalAmount,\n shippingCostAmount,\n shipping,\n customFields: cf,\n });\n\n try {\n const resend = new Resend(resendKey);\n const { error } = await resend.emails.send({\n from: orderEmailFrom,\n to: email,\n ...(orderEmailBcc ? { bcc: orderEmailBcc } : {}),\n ...(orderEmailReplyTo ? { reply_to: orderEmailReplyTo } : {}),\n subject,\n html,\n });\n if (error) {\n console.error(\"[webhook] Resend error:\", providerErrorDetails(error));\n }\n } catch (e) {\n console.error(\"[webhook] Resend exception:\", providerErrorDetails(e));\n }\n\n return new Response(\"OK\", { status: 200 });\n };\n}\n"],"mappings":"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","names":["paymentIntent"]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/notify.ts"],"sourcesContent":["import { Resend } from \"resend\";\n\nconst DAY_MS = 24 * 60 * 60 * 1000;\n\nexport type UnshippedOrder = {\n id: string;\n name: string | null;\n items_json: string | null;\n amount: number | null;\n created_at: number | string | null;\n};\n\nexport type NotifyEnv = {\n DISCORD_WEBHOOK_URL?: string;\n RESEND_API_KEY?: string;\n};\n\ntype CheckoutItem = {\n sku?: unknown;\n quantity?: unknown;\n};\n\nexport type NotifyProduct = {\n name: string;\n};\n\nexport type NotifyProductsMap = Record<string, NotifyProduct | undefined>;\n\nexport type NotifyUnshippedOptions = {\n alertFrom: string;\n alertTo: string;\n products?: NotifyProductsMap;\n};\n\nfunction formatYen(value: number | null) {\n if (value == null) return \"-\";\n return new Intl.NumberFormat(\"ja-JP\", {\n style: \"currency\",\n currency: \"JPY\",\n maximumFractionDigits: 0,\n }).format(value);\n}\n\nfunction parseTimestamp(value: number | string | null) {\n if (value == null || value === \"\") return null;\n const timestamp = typeof value === \"string\" ? Number(value) : value;\n return Number.isFinite(timestamp) ? timestamp : null;\n}\n\nfunction parseItems(itemsJson: string | null, products: NotifyProductsMap) {\n if (!itemsJson) return \"-\";\n\n try {\n const parsed: unknown = JSON.parse(itemsJson);\n if (!Array.isArray(parsed) || parsed.length === 0) return \"-\";\n\n return parsed\n .map((item) => {\n if (typeof item !== \"object\" || item === null) return null;\n const { sku, quantity } = item as CheckoutItem;\n const productSku = typeof sku === \"string\" ? sku : \"\";\n const productName = productSku && products[productSku] ? products[productSku].name : productSku || \"商品\";\n const itemQuantity = typeof quantity === \"number\" && Number.isFinite(quantity) ? quantity : 0;\n return `${productName} x ${itemQuantity}`;\n })\n .filter((item): item is string => Boolean(item))\n .join(\", \");\n } catch {\n return \"-\";\n }\n}\n\nfunction escapeHtml(value: string) {\n return value\n .replaceAll(\"&\", \"&amp;\")\n .replaceAll(\"<\", \"&lt;\")\n .replaceAll(\">\", \"&gt;\")\n .replaceAll('\"', \"&quot;\")\n .replaceAll(\"'\", \"&#39;\");\n}\n\nfunction notificationErrorSuffix(error: unknown) {\n if (typeof error !== \"object\" || error === null) return \"\";\n const value = error as { name?: unknown; statusCode?: unknown };\n const details = [\n typeof value.name === \"string\" ? `name=${value.name}` : null,\n typeof value.statusCode === \"number\" ? `statusCode=${value.statusCode}` : null,\n ].filter((detail): detail is string => detail !== null);\n\n return details.length > 0 ? ` (${details.join(\", \")})` : \"\";\n}\n\nfunction formatOrderLine(order: UnshippedOrder, products: NotifyProductsMap, now = Date.now()) {\n const createdAt = parseTimestamp(order.created_at);\n const elapsedDays = createdAt == null ? \"-\" : String(Math.floor((now - createdAt) / DAY_MS));\n const name = order.name?.trim() || \"お客様\";\n return `${name} | ${parseItems(order.items_json, products)} | ${formatYen(order.amount)} | ${elapsedDays}日`;\n}\n\nexport async function notifyUnshipped(\n orders: UnshippedOrder[],\n env: NotifyEnv,\n { alertFrom, alertTo, products = {} }: NotifyUnshippedOptions,\n) {\n if (orders.length === 0) return;\n if (!env.DISCORD_WEBHOOK_URL) throw new Error(\"DISCORD_WEBHOOK_URL is not configured\");\n if (!env.RESEND_API_KEY) throw new Error(\"RESEND_API_KEY is not configured\");\n\n const lines = orders.map((order) => formatOrderLine(order, products));\n const content = [\"未発送アラート: pending の注文があります。\", \"\", ...lines].join(\"\\n\");\n\n const discordResponse = await fetch(env.DISCORD_WEBHOOK_URL, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ content, allowed_mentions: { parse: [] } }),\n });\n if (!discordResponse.ok) {\n throw new Error(`Discord notification failed: ${discordResponse.status}`);\n }\n\n const rows = lines\n .map((line) => {\n const [name, items, amount, elapsedDays] = line.split(\" | \");\n return `<tr>\n <td style=\"padding:8px;border-bottom:1px solid #eee;\">${escapeHtml(name ?? \"\")}</td>\n <td style=\"padding:8px;border-bottom:1px solid #eee;\">${escapeHtml(items ?? \"\")}</td>\n <td style=\"padding:8px;border-bottom:1px solid #eee;text-align:right;\">${escapeHtml(amount ?? \"\")}</td>\n <td style=\"padding:8px;border-bottom:1px solid #eee;text-align:right;\">${escapeHtml(elapsedDays ?? \"\")}</td>\n </tr>`;\n })\n .join(\"\");\n\n const resend = new Resend(env.RESEND_API_KEY);\n const { error } = await resend.emails.send({\n from: alertFrom,\n to: alertTo,\n subject: \"【emu】未発送アラート\",\n html: `<!DOCTYPE html>\n<html lang=\"ja\">\n<head><meta charset=\"UTF-8\"></head>\n<body style=\"font-family:sans-serif;color:#222;max-width:720px;margin:0 auto;padding:32px 16px;\">\n <h1 style=\"font-size:20px;margin:0 0 16px;\">未発送アラート</h1>\n <p style=\"font-size:14px;line-height:1.7;color:#555;\">pending のまま threshold を超えた注文があります。</p>\n <table style=\"width:100%;border-collapse:collapse;font-size:14px;\">\n <thead>\n <tr>\n <th style=\"padding:8px;border-bottom:2px solid #222;text-align:left;\">名前</th>\n <th style=\"padding:8px;border-bottom:2px solid #222;text-align:left;\">商品</th>\n <th style=\"padding:8px;border-bottom:2px solid #222;text-align:right;\">金額</th>\n <th style=\"padding:8px;border-bottom:2px solid #222;text-align:right;\">経過日数</th>\n </tr>\n </thead>\n <tbody>${rows}</tbody>\n </table>\n</body>\n</html>`,\n text: content,\n });\n if (error) {\n throw new Error(`Resend notification failed${notificationErrorSuffix(error)}`);\n }\n}\n"],"mappings":"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","names":[]}