includio-cms 0.24.0 → 0.25.0

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.
Files changed (90) hide show
  1. package/API.md +29 -6
  2. package/CHANGELOG.md +95 -0
  3. package/DOCS.md +80 -5
  4. package/ROADMAP.md +1 -0
  5. package/dist/admin/client/index.d.ts +3 -0
  6. package/dist/admin/client/index.js +3 -0
  7. package/dist/admin/client/shop/coupon-edit-page.svelte +44 -0
  8. package/dist/admin/client/shop/coupon-edit-page.svelte.d.ts +3 -0
  9. package/dist/admin/client/shop/coupon-form.svelte +170 -0
  10. package/dist/admin/client/shop/coupon-form.svelte.d.ts +18 -0
  11. package/dist/admin/client/shop/coupon-new-page.svelte +25 -0
  12. package/dist/admin/client/shop/coupon-new-page.svelte.d.ts +18 -0
  13. package/dist/admin/client/shop/coupons-list-page.svelte +135 -0
  14. package/dist/admin/client/shop/coupons-list-page.svelte.d.ts +3 -0
  15. package/dist/admin/client/shop/refund-dialog.svelte +161 -0
  16. package/dist/admin/client/shop/refund-dialog.svelte.d.ts +11 -0
  17. package/dist/admin/client/shop/shipping-method-edit-page.svelte +3 -6
  18. package/dist/admin/client/shop/shipping-method-form.svelte +15 -21
  19. package/dist/admin/client/shop/shipping-method-new-page.svelte +3 -6
  20. package/dist/admin/client/shop/shipping-methods-list-page.svelte +6 -6
  21. package/dist/admin/client/shop/shop-order-detail-page.svelte +107 -27
  22. package/dist/admin/client/shop/shop-orders-list-page.svelte +49 -11
  23. package/dist/admin/client/shop/shop-products-list-page.svelte +12 -11
  24. package/dist/admin/components/layout/lang.d.ts +1 -0
  25. package/dist/admin/components/layout/lang.js +4 -2
  26. package/dist/admin/components/layout/layout-renderer.svelte +12 -11
  27. package/dist/admin/components/layout/nav-breadcrumbs.svelte +3 -5
  28. package/dist/admin/components/layout/nav-shop.svelte +3 -1
  29. package/dist/admin/components/layout/nav-user.svelte +6 -4
  30. package/dist/admin/components/layout/site-header.svelte +11 -5
  31. package/dist/admin/remote/shop.remote.d.ts +122 -3
  32. package/dist/admin/remote/shop.remote.js +161 -5
  33. package/dist/db-postgres/schema/shop/couponRedemptions.d.ts +97 -0
  34. package/dist/db-postgres/schema/shop/couponRedemptions.js +21 -0
  35. package/dist/db-postgres/schema/shop/coupons.d.ts +197 -0
  36. package/dist/db-postgres/schema/shop/coupons.js +18 -0
  37. package/dist/db-postgres/schema/shop/index.d.ts +4 -0
  38. package/dist/db-postgres/schema/shop/index.js +4 -0
  39. package/dist/db-postgres/schema/shop/product.d.ts +17 -0
  40. package/dist/db-postgres/schema/shop/product.js +2 -0
  41. package/dist/db-postgres/schema/shop/refunds.d.ts +214 -0
  42. package/dist/db-postgres/schema/shop/refunds.js +21 -0
  43. package/dist/db-postgres/schema/shop/webhookEvents.d.ts +183 -0
  44. package/dist/db-postgres/schema/shop/webhookEvents.js +22 -0
  45. package/dist/shop/adapters/payu/client.d.ts +9 -0
  46. package/dist/shop/adapters/payu/client.js +29 -0
  47. package/dist/shop/adapters/payu/index.js +17 -1
  48. package/dist/shop/adapters/stripe/index.d.ts +64 -0
  49. package/dist/shop/adapters/stripe/index.js +169 -0
  50. package/dist/shop/adapters/stripe/payload.d.ts +38 -0
  51. package/dist/shop/adapters/stripe/payload.js +90 -0
  52. package/dist/shop/adapters/stripe/status-map.d.ts +11 -0
  53. package/dist/shop/adapters/stripe/status-map.js +31 -0
  54. package/dist/shop/cart/coupon-cookie.d.ts +7 -0
  55. package/dist/shop/cart/coupon-cookie.js +32 -0
  56. package/dist/shop/cart/types.d.ts +12 -0
  57. package/dist/shop/client/index.d.ts +118 -0
  58. package/dist/shop/client/index.js +39 -1
  59. package/dist/shop/http/cart-handler.d.ts +8 -0
  60. package/dist/shop/http/cart-handler.js +60 -1
  61. package/dist/shop/http/checkout-handler.js +7 -3
  62. package/dist/shop/http/index.d.ts +1 -1
  63. package/dist/shop/http/index.js +1 -1
  64. package/dist/shop/http/retry-payment-handler.js +1 -1
  65. package/dist/shop/http/webhook-handler.js +19 -1
  66. package/dist/shop/http/webhook-idempotency.d.ts +16 -0
  67. package/dist/shop/http/webhook-idempotency.js +51 -0
  68. package/dist/shop/http/webhook-logic.js +2 -1
  69. package/dist/shop/index.d.ts +3 -1
  70. package/dist/shop/index.js +3 -1
  71. package/dist/shop/pricing.d.ts +15 -0
  72. package/dist/shop/pricing.js +22 -0
  73. package/dist/shop/server/cart-hydrate.d.ts +1 -0
  74. package/dist/shop/server/cart-hydrate.js +58 -10
  75. package/dist/shop/server/coupons.d.ts +53 -0
  76. package/dist/shop/server/coupons.js +117 -0
  77. package/dist/shop/server/email.d.ts +15 -0
  78. package/dist/shop/server/email.js +46 -3
  79. package/dist/shop/server/orders.d.ts +1 -0
  80. package/dist/shop/server/orders.js +120 -54
  81. package/dist/shop/server/refund.d.ts +32 -0
  82. package/dist/shop/server/refund.js +140 -0
  83. package/dist/shop/svelte/InpostPicker.svelte +4 -7
  84. package/dist/shop/svelte/OrderStatus.svelte +6 -10
  85. package/dist/shop/svelte/labels.js +4 -2
  86. package/dist/shop/types.d.ts +41 -1
  87. package/dist/updates/0.25.0/index.d.ts +2 -0
  88. package/dist/updates/0.25.0/index.js +89 -0
  89. package/dist/updates/index.js +64 -1
  90. package/package.json +6 -1
@@ -0,0 +1,169 @@
1
+ import { requireShopConfig } from '../../server/db.js';
2
+ import { getOrderById } from '../../server/orders.js';
3
+ import { buildOrderViewUrl } from '../../server/order-access-url.js';
4
+ import { buildStripeCheckoutPayload } from './payload.js';
5
+ import { mapStripeEventType, mapStripeSessionStatus } from './status-map.js';
6
+ const DEFAULT_LABEL = { pl: 'Karta płatnicza (Stripe)', en: 'Card payment (Stripe)' };
7
+ const DEFAULT_PRODUCT_NAME = { pl: 'Zamówienie {number}', en: 'Order {number}' };
8
+ const DEFAULT_API_VERSION = '2024-11-20.acacia';
9
+ function pickI18n(text, language, fallback = 'pl') {
10
+ if (language && text[language])
11
+ return text[language];
12
+ return text[fallback] ?? Object.values(text)[0] ?? '';
13
+ }
14
+ /**
15
+ * Stripe payment adapter (Checkout Session flow).
16
+ *
17
+ * `stripe` is an **optional peer dependency** — install it in your project
18
+ * (`pnpm add stripe`) when using this adapter. The SDK is lazy-loaded on first
19
+ * `createPayment()` / `getStatus()` / `refund()` / `handleWebhook()` call;
20
+ * a missing peer throws a clear error.
21
+ *
22
+ * @public
23
+ * @example
24
+ * ```ts
25
+ * import { stripeAdapter } from 'includio-cms/shop';
26
+ *
27
+ * const stripe = stripeAdapter({
28
+ * secretKey: process.env.STRIPE_SECRET_KEY!,
29
+ * webhookSecret: process.env.STRIPE_WEBHOOK_SECRET!,
30
+ * successUrl: 'https://example.com/shop/order/{orderNumber}?token={accessToken}',
31
+ * cancelUrl: 'https://example.com/shop/cancelled'
32
+ * });
33
+ * ```
34
+ */
35
+ export function stripeAdapter(opts) {
36
+ const id = opts.id ?? 'stripe';
37
+ const apiVersion = opts.apiVersion ?? DEFAULT_API_VERSION;
38
+ let cachedClient = null;
39
+ let cachedStripeNs = null;
40
+ async function loadStripeNs() {
41
+ if (cachedStripeNs)
42
+ return cachedStripeNs;
43
+ const importer = opts.stripeImport ?? (() => import('stripe'));
44
+ try {
45
+ cachedStripeNs = await importer();
46
+ }
47
+ catch {
48
+ throw new Error('stripe SDK is required for stripeAdapter — install it with `pnpm add stripe`');
49
+ }
50
+ return cachedStripeNs;
51
+ }
52
+ async function getClient() {
53
+ if (cachedClient)
54
+ return cachedClient;
55
+ const ns = await loadStripeNs();
56
+ const Ctor = ns.default;
57
+ cachedClient = new Ctor(opts.secretKey, {
58
+ apiVersion: apiVersion
59
+ });
60
+ return cachedClient;
61
+ }
62
+ return {
63
+ id,
64
+ label: opts.label ?? DEFAULT_LABEL,
65
+ async createPayment(order, ctx) {
66
+ const shop = requireShopConfig();
67
+ const successTemplate = opts.successUrl ?? shop.orderViewUrl;
68
+ const cancelTemplate = opts.cancelUrl ?? successTemplate;
69
+ if (!/^https?:\/\//i.test(successTemplate)) {
70
+ throw new Error('stripeAdapter: successUrl (or shop.orderViewUrl) must be an absolute URL.');
71
+ }
72
+ if (!/^https?:\/\//i.test(cancelTemplate)) {
73
+ throw new Error('stripeAdapter: cancelUrl must be an absolute URL.');
74
+ }
75
+ const full = await getOrderById(order.id);
76
+ const language = ctx?.language ?? full?.language ?? null;
77
+ const successUrl = buildOrderViewUrl(successTemplate, {
78
+ orderNumber: order.number,
79
+ orderId: order.id,
80
+ accessToken: full?.accessToken ?? '',
81
+ language
82
+ });
83
+ const cancelUrl = buildOrderViewUrl(cancelTemplate, {
84
+ orderNumber: order.number,
85
+ orderId: order.id,
86
+ accessToken: full?.accessToken ?? '',
87
+ language
88
+ });
89
+ const productNameTpl = pickI18n(opts.productName ?? DEFAULT_PRODUCT_NAME, language);
90
+ const productName = productNameTpl.replace('{number}', order.number);
91
+ const payload = buildStripeCheckoutPayload({
92
+ order,
93
+ successUrl,
94
+ cancelUrl,
95
+ productName,
96
+ language
97
+ });
98
+ const client = await getClient();
99
+ const session = await client.checkout.sessions.create(payload);
100
+ if (!session.url) {
101
+ throw new Error('Stripe Checkout Session created without redirect URL.');
102
+ }
103
+ return {
104
+ status: 'redirect',
105
+ redirectUrl: session.url,
106
+ providerRef: session.id
107
+ };
108
+ },
109
+ async getStatus(providerRef) {
110
+ const client = await getClient();
111
+ const session = await client.checkout.sessions.retrieve(providerRef);
112
+ const orderNumber = session.metadata?.orderNumber ?? session.client_reference_id ?? '';
113
+ return {
114
+ orderNumber,
115
+ status: mapStripeSessionStatus(session.status, session.payment_status),
116
+ providerRef: session.id,
117
+ raw: session
118
+ };
119
+ },
120
+ async handleWebhook(req) {
121
+ const rawBody = await req.text();
122
+ const signature = req.headers.get('stripe-signature');
123
+ if (!signature)
124
+ throw new Error('Stripe webhook missing Stripe-Signature header');
125
+ const ns = await loadStripeNs();
126
+ const client = await getClient();
127
+ let event;
128
+ try {
129
+ event = client.webhooks.constructEvent(rawBody, signature, opts.webhookSecret);
130
+ }
131
+ catch (err) {
132
+ throw new Error(`Stripe webhook signature verification failed: ${err.message}`);
133
+ }
134
+ void ns; // satisfy unused warning when stripeImport mocks `default`
135
+ const session = event.data?.object ?? null;
136
+ const orderNumber = session?.metadata?.orderNumber ?? session?.client_reference_id ?? '';
137
+ const mapped = mapStripeEventType(event.type, session?.status ?? null, session?.payment_status ?? null);
138
+ return {
139
+ orderNumber,
140
+ status: mapped ?? 'pending',
141
+ providerRef: session?.id ?? event.id,
142
+ raw: event,
143
+ eventId: event.id,
144
+ eventType: event.type
145
+ };
146
+ },
147
+ async refund(input) {
148
+ const client = await getClient();
149
+ // providerRef is the Checkout Session id; resolve to PaymentIntent for refund.
150
+ const session = await client.checkout.sessions.retrieve(input.providerRef);
151
+ const paymentIntent = typeof session.payment_intent === 'string'
152
+ ? session.payment_intent
153
+ : (session.payment_intent?.id ?? null);
154
+ if (!paymentIntent) {
155
+ throw new Error(`Stripe refund: no payment_intent attached to session ${input.providerRef}`);
156
+ }
157
+ const refund = await client.refunds.create({
158
+ payment_intent: paymentIntent,
159
+ ...(input.amount !== undefined ? { amount: input.amount } : {}),
160
+ ...(input.reason ? { metadata: { reason: input.reason } } : {})
161
+ });
162
+ return {
163
+ providerRef: refund.id,
164
+ amount: typeof refund.amount === 'number' ? refund.amount : (input.amount ?? 0),
165
+ raw: refund
166
+ };
167
+ }
168
+ };
169
+ }
@@ -0,0 +1,38 @@
1
+ import type { OrderRef } from '../../types.js';
2
+ export interface StripeCheckoutPayload {
3
+ mode: 'payment';
4
+ line_items: Array<{
5
+ price_data: {
6
+ currency: string;
7
+ product_data: {
8
+ name: string;
9
+ };
10
+ unit_amount: number;
11
+ };
12
+ quantity: number;
13
+ }>;
14
+ success_url: string;
15
+ cancel_url: string;
16
+ customer_email?: string;
17
+ client_reference_id: string;
18
+ metadata: Record<string, string>;
19
+ payment_intent_data?: {
20
+ metadata: Record<string, string>;
21
+ };
22
+ locale?: string;
23
+ }
24
+ export declare function normalizeStripeLocale(input: string | null | undefined): string | undefined;
25
+ export interface BuildStripeCheckoutPayloadInput {
26
+ order: OrderRef;
27
+ successUrl: string;
28
+ cancelUrl: string;
29
+ productName: string;
30
+ language?: string | null;
31
+ metadata?: Record<string, string>;
32
+ }
33
+ /**
34
+ * Build a Checkout Session create payload representing the order as a single
35
+ * line item (full gross). We intentionally do NOT itemise — the order snapshot
36
+ * already lives in our DB; Stripe's job is collect payment for `totalGross`.
37
+ */
38
+ export declare function buildStripeCheckoutPayload(input: BuildStripeCheckoutPayloadInput): StripeCheckoutPayload;
@@ -0,0 +1,90 @@
1
+ // All entries lowercase; we normalize input to lowercase before lookup.
2
+ const STRIPE_LOCALES = new Set([
3
+ 'auto',
4
+ 'bg',
5
+ 'cs',
6
+ 'da',
7
+ 'de',
8
+ 'el',
9
+ 'en',
10
+ 'en-gb',
11
+ 'es',
12
+ 'es-419',
13
+ 'et',
14
+ 'fi',
15
+ 'fil',
16
+ 'fr',
17
+ 'fr-ca',
18
+ 'hr',
19
+ 'hu',
20
+ 'id',
21
+ 'it',
22
+ 'ja',
23
+ 'ko',
24
+ 'lt',
25
+ 'lv',
26
+ 'ms',
27
+ 'mt',
28
+ 'nb',
29
+ 'nl',
30
+ 'pl',
31
+ 'pt',
32
+ 'pt-br',
33
+ 'ro',
34
+ 'ru',
35
+ 'sk',
36
+ 'sl',
37
+ 'sv',
38
+ 'th',
39
+ 'tr',
40
+ 'vi',
41
+ 'zh',
42
+ 'zh-hk',
43
+ 'zh-tw'
44
+ ]);
45
+ export function normalizeStripeLocale(input) {
46
+ if (!input)
47
+ return undefined;
48
+ const lower = input.toLowerCase();
49
+ if (STRIPE_LOCALES.has(lower))
50
+ return lower;
51
+ const root = lower.split('-')[0];
52
+ if (STRIPE_LOCALES.has(root))
53
+ return root;
54
+ return undefined;
55
+ }
56
+ /**
57
+ * Build a Checkout Session create payload representing the order as a single
58
+ * line item (full gross). We intentionally do NOT itemise — the order snapshot
59
+ * already lives in our DB; Stripe's job is collect payment for `totalGross`.
60
+ */
61
+ export function buildStripeCheckoutPayload(input) {
62
+ const metadata = {
63
+ orderNumber: input.order.number,
64
+ orderId: input.order.id,
65
+ ...input.metadata
66
+ };
67
+ const payload = {
68
+ mode: 'payment',
69
+ line_items: [
70
+ {
71
+ price_data: {
72
+ currency: input.order.currency.toLowerCase(),
73
+ product_data: { name: input.productName },
74
+ unit_amount: input.order.totalGross
75
+ },
76
+ quantity: 1
77
+ }
78
+ ],
79
+ success_url: input.successUrl,
80
+ cancel_url: input.cancelUrl,
81
+ customer_email: input.order.customerEmail,
82
+ client_reference_id: input.order.number,
83
+ metadata,
84
+ payment_intent_data: { metadata }
85
+ };
86
+ const locale = normalizeStripeLocale(input.language);
87
+ if (locale)
88
+ payload.locale = locale;
89
+ return payload;
90
+ }
@@ -0,0 +1,11 @@
1
+ import type { PaymentEvent } from '../../types.js';
2
+ /**
3
+ * Map a Stripe Checkout Session payment_status (paid / unpaid / no_payment_required)
4
+ * combined with session.status (open / complete / expired) into our PaymentEvent.status.
5
+ */
6
+ export declare function mapStripeSessionStatus(sessionStatus: string | null | undefined, paymentStatus: string | null | undefined): PaymentEvent['status'];
7
+ /**
8
+ * Map a Stripe webhook event type to a PaymentEvent.status.
9
+ * Returns null when the event is not relevant to order status (e.g. fulfilment-only).
10
+ */
11
+ export declare function mapStripeEventType(eventType: string, sessionStatus: string | null | undefined, paymentStatus: string | null | undefined): PaymentEvent['status'] | null;
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Map a Stripe Checkout Session payment_status (paid / unpaid / no_payment_required)
3
+ * combined with session.status (open / complete / expired) into our PaymentEvent.status.
4
+ */
5
+ export function mapStripeSessionStatus(sessionStatus, paymentStatus) {
6
+ if (sessionStatus === 'expired')
7
+ return 'paymentRejected';
8
+ if (sessionStatus === 'complete') {
9
+ if (paymentStatus === 'paid' || paymentStatus === 'no_payment_required')
10
+ return 'paid';
11
+ // async pending (e.g. SEPA) — keep order in pending until follow-up event
12
+ return 'pending';
13
+ }
14
+ return 'pending';
15
+ }
16
+ /**
17
+ * Map a Stripe webhook event type to a PaymentEvent.status.
18
+ * Returns null when the event is not relevant to order status (e.g. fulfilment-only).
19
+ */
20
+ export function mapStripeEventType(eventType, sessionStatus, paymentStatus) {
21
+ switch (eventType) {
22
+ case 'checkout.session.completed':
23
+ case 'checkout.session.async_payment_succeeded':
24
+ return mapStripeSessionStatus(sessionStatus, paymentStatus);
25
+ case 'checkout.session.expired':
26
+ case 'checkout.session.async_payment_failed':
27
+ return 'paymentRejected';
28
+ default:
29
+ return null;
30
+ }
31
+ }
@@ -0,0 +1,7 @@
1
+ import type { CartCookies } from './types.js';
2
+ export declare const COUPON_COOKIE_NAME = "aria_shop_coupon";
3
+ export declare function normalizeCouponCode(input: string): string;
4
+ export declare function isValidCouponCode(code: string): boolean;
5
+ export declare function readCouponCookie(cookies: CartCookies): string | null;
6
+ export declare function writeCouponCookie(cookies: CartCookies, code: string): void;
7
+ export declare function clearCouponCookie(cookies: CartCookies): void;
@@ -0,0 +1,32 @@
1
+ export const COUPON_COOKIE_NAME = 'aria_shop_coupon';
2
+ const MAX_AGE_SECONDS = 60 * 60 * 24 * 7; // 7 days — shorter than cart TTL by design
3
+ const MAX_LEN = 64;
4
+ const VALID = /^[A-Z0-9_-]{1,64}$/;
5
+ export function normalizeCouponCode(input) {
6
+ return input.trim().toUpperCase();
7
+ }
8
+ export function isValidCouponCode(code) {
9
+ return code.length > 0 && code.length <= MAX_LEN && VALID.test(code);
10
+ }
11
+ export function readCouponCookie(cookies) {
12
+ const raw = cookies.get(COUPON_COOKIE_NAME);
13
+ if (!raw)
14
+ return null;
15
+ const code = normalizeCouponCode(raw);
16
+ return isValidCouponCode(code) ? code : null;
17
+ }
18
+ export function writeCouponCookie(cookies, code) {
19
+ const normalized = normalizeCouponCode(code);
20
+ if (!isValidCouponCode(normalized))
21
+ return;
22
+ cookies.set(COUPON_COOKIE_NAME, normalized, {
23
+ path: '/',
24
+ maxAge: MAX_AGE_SECONDS,
25
+ httpOnly: false,
26
+ sameSite: 'lax',
27
+ secure: false
28
+ });
29
+ }
30
+ export function clearCouponCookie(cookies) {
31
+ cookies.delete(COUPON_COOKIE_NAME, { path: '/' });
32
+ }
@@ -19,13 +19,25 @@ export interface CartLine extends CartItemRef {
19
19
  available: boolean;
20
20
  issue?: 'not-found' | 'inactive' | 'out-of-stock' | 'qty-adjusted';
21
21
  }
22
+ export interface AppliedCoupon {
23
+ code: string;
24
+ type: 'percent' | 'fixed';
25
+ value: number;
26
+ discountNet: number;
27
+ discountGross: number;
28
+ }
22
29
  export interface CartSnapshot {
23
30
  items: CartLine[];
24
31
  itemCount: number;
32
+ /** Subtotal before coupon (sum of lines). When no coupon, equals totalNet. */
33
+ subtotalNet: number;
34
+ subtotalGross: number;
35
+ subtotalVat: number;
25
36
  totalNet: number;
26
37
  totalGross: number;
27
38
  totalVat: number;
28
39
  currency: Currency;
40
+ coupon?: AppliedCoupon;
29
41
  }
30
42
  export interface CartCookies {
31
43
  get(name: string): string | undefined;
@@ -1,7 +1,22 @@
1
1
  import type { CartSnapshot } from '../cart/types.js';
2
2
  import type { OrderStatus } from '../types.js';
3
+ /**
4
+ * Options for `createShopClient()`.
5
+ *
6
+ * @public
7
+ */
3
8
  export interface ShopClientOptions {
9
+ /**
10
+ * Base URL for the shop REST API. Empty string (default) means same-origin —
11
+ * paths like `/api/shop/cart` resolve relative to the current document.
12
+ * Provide an absolute URL when calling from a different origin (e.g. a
13
+ * mobile app or static site hitting your CMS over HTTPS).
14
+ */
4
15
  baseUrl?: string;
16
+ /**
17
+ * Custom `fetch` implementation. Defaults to `globalThis.fetch`. Override
18
+ * to inject auth headers, request logging, or to use `node-fetch` in tests.
19
+ */
5
20
  fetch?: typeof fetch;
6
21
  }
7
22
  export interface ShippingMethodPublic {
@@ -99,28 +114,131 @@ export interface RetryPaymentResult {
99
114
  requiresPaymentRedirect: boolean;
100
115
  redirectUrl: string | null;
101
116
  }
117
+ /**
118
+ * Headless shop SDK surface returned by `createShopClient()`.
119
+ *
120
+ * Every method maps 1:1 to a REST endpoint mounted by the shop HTTP handlers.
121
+ * The shape is stable across patch versions; new methods may be added under
122
+ * existing namespaces in minor releases.
123
+ *
124
+ * @public
125
+ */
102
126
  export interface ShopClient {
127
+ /** Cart operations — read state and mutate items / coupon. */
103
128
  cart: {
129
+ /**
130
+ * Get the current cart snapshot, including hydrated line totals,
131
+ * subtotal, and any applied coupon.
132
+ *
133
+ * @returns Server-authoritative cart snapshot.
134
+ */
104
135
  get(): Promise<CartSnapshot>;
136
+ /**
137
+ * Add `qty` units of a variant to the cart. Existing entries for the
138
+ * same `variantId` are merged (qty summed, capped at server max).
139
+ *
140
+ * @param variantId - The product variant id.
141
+ * @param qty - Quantity to add. Defaults to 1.
142
+ */
105
143
  add(variantId: string, qty?: number): Promise<CartSnapshot>;
144
+ /**
145
+ * Set the absolute quantity of an existing line. `qty <= 0` removes
146
+ * the line.
147
+ */
106
148
  update(variantId: string, qty: number): Promise<CartSnapshot>;
149
+ /** Remove a single variant from the cart. */
107
150
  remove(variantId: string): Promise<CartSnapshot>;
151
+ /** Empty the cart and drop any applied coupon. */
108
152
  clear(): Promise<CartSnapshot>;
153
+ /**
154
+ * Apply a coupon code to the current cart. Codes are case-insensitive
155
+ * and validated server-side (existence, expiry, max-uses, min-order).
156
+ * Throws on invalid codes — use a try/catch and surface the message.
157
+ *
158
+ * @param code - The coupon code (e.g. `"ARIA10"`).
159
+ */
160
+ applyCoupon(code: string): Promise<CartSnapshot>;
161
+ /** Remove the currently-applied coupon (if any). Always succeeds. */
162
+ removeCoupon(): Promise<CartSnapshot>;
109
163
  };
164
+ /** Shipping method discovery. */
110
165
  shipping: {
166
+ /**
167
+ * List all active shipping methods configured by the shop, with
168
+ * pricing and carrier metadata. Public — usable on the storefront.
169
+ */
111
170
  list(): Promise<{
112
171
  items: ShippingMethodPublic[];
113
172
  }>;
114
173
  };
174
+ /** Checkout flow — converts the cart into an order. */
115
175
  checkout: {
176
+ /**
177
+ * Submit the cart as an order. Validates consents, stock, coupons,
178
+ * carrier selection, and initiates the chosen payment adapter. Returns
179
+ * a redirect URL when the payment provider needs to take over (e.g.
180
+ * Stripe Checkout, PayU), or `paymentStatus: 'manual'` for manual
181
+ * methods (bank transfer).
182
+ */
116
183
  submit(input: CheckoutInput): Promise<CheckoutResult>;
117
184
  };
185
+ /** Order lookup + payment retry/refresh from the customer side. */
118
186
  orders: {
187
+ /**
188
+ * Fetch full order detail. Pass the customer's `accessToken` (returned
189
+ * from checkout) to bypass the same-browser cookie fallback.
190
+ */
119
191
  get(orderNumber: string, token?: string): Promise<OrderDetailResponse>;
192
+ /**
193
+ * Re-poll the payment provider for the latest status — useful when a
194
+ * webhook is lost or the customer returns from a redirect before the
195
+ * status webhook arrived.
196
+ */
120
197
  refreshPayment(orderNumber: string, token?: string): Promise<RefreshPaymentResult>;
198
+ /**
199
+ * Initiate a fresh payment attempt for an unpaid order — typical use
200
+ * is recovering from `paymentRejected` without making the customer
201
+ * re-fill the checkout form.
202
+ */
121
203
  retryPayment(orderNumber: string, token?: string): Promise<RetryPaymentResult>;
122
204
  };
123
205
  }
206
+ /**
207
+ * Create a typed, isomorphic shop SDK client.
208
+ *
209
+ * The client is a thin REST wrapper — every call hits an HTTP endpoint
210
+ * mounted by the shop handlers (`createCartHandler`, `createCheckoutHandler`,
211
+ * etc.). Use it from SvelteKit pages, server endpoints, mobile apps, or any
212
+ * environment with a `fetch` implementation.
213
+ *
214
+ * Errors from the shop API are surfaced as `Error` with the server-provided
215
+ * `error` field as the message.
216
+ *
217
+ * @param options - Optional `baseUrl` (default same-origin) and custom `fetch`.
218
+ * @returns A `ShopClient` with `.cart`, `.shipping`, `.checkout`, `.orders`.
219
+ * @public
220
+ * @example
221
+ * ```ts
222
+ * import { createShopClient } from 'includio-cms/shop/client';
223
+ *
224
+ * const shop = createShopClient({ baseUrl: '' });
225
+ *
226
+ * await shop.cart.add(variantId);
227
+ * await shop.cart.applyCoupon('ARIA10');
228
+ * const cart = await shop.cart.get();
229
+ *
230
+ * const result = await shop.checkout.submit({
231
+ * customerEmail: 'buyer@example.com',
232
+ * shippingMethodId,
233
+ * paymentMethod: 'stripe',
234
+ * consents: [{ id: 'tos', accepted: true }]
235
+ * });
236
+ *
237
+ * if (result.requiresPaymentRedirect && result.redirectUrl) {
238
+ * window.location.href = result.redirectUrl;
239
+ * }
240
+ * ```
241
+ */
124
242
  export declare function createShopClient(options?: ShopClientOptions): ShopClient;
125
243
  export type { CartSnapshot, CartLine, CartItemRef } from '../cart/types.js';
126
244
  export { createOrderState } from './use-order.svelte.js';
@@ -1,3 +1,39 @@
1
+ /**
2
+ * Create a typed, isomorphic shop SDK client.
3
+ *
4
+ * The client is a thin REST wrapper — every call hits an HTTP endpoint
5
+ * mounted by the shop handlers (`createCartHandler`, `createCheckoutHandler`,
6
+ * etc.). Use it from SvelteKit pages, server endpoints, mobile apps, or any
7
+ * environment with a `fetch` implementation.
8
+ *
9
+ * Errors from the shop API are surfaced as `Error` with the server-provided
10
+ * `error` field as the message.
11
+ *
12
+ * @param options - Optional `baseUrl` (default same-origin) and custom `fetch`.
13
+ * @returns A `ShopClient` with `.cart`, `.shipping`, `.checkout`, `.orders`.
14
+ * @public
15
+ * @example
16
+ * ```ts
17
+ * import { createShopClient } from 'includio-cms/shop/client';
18
+ *
19
+ * const shop = createShopClient({ baseUrl: '' });
20
+ *
21
+ * await shop.cart.add(variantId);
22
+ * await shop.cart.applyCoupon('ARIA10');
23
+ * const cart = await shop.cart.get();
24
+ *
25
+ * const result = await shop.checkout.submit({
26
+ * customerEmail: 'buyer@example.com',
27
+ * shippingMethodId,
28
+ * paymentMethod: 'stripe',
29
+ * consents: [{ id: 'tos', accepted: true }]
30
+ * });
31
+ *
32
+ * if (result.requiresPaymentRedirect && result.redirectUrl) {
33
+ * window.location.href = result.redirectUrl;
34
+ * }
35
+ * ```
36
+ */
1
37
  export function createShopClient(options = {}) {
2
38
  const base = (options.baseUrl ?? '').replace(/\/$/, '');
3
39
  const fetchFn = options.fetch ?? globalThis.fetch.bind(globalThis);
@@ -28,7 +64,9 @@ export function createShopClient(options = {}) {
28
64
  add: (variantId, qty = 1) => call('POST', '/api/shop/cart', { variantId, qty }),
29
65
  update: (variantId, qty) => call('PATCH', '/api/shop/cart', { variantId, qty }),
30
66
  remove: (variantId) => call('DELETE', `/api/shop/cart?variantId=${encodeURIComponent(variantId)}`),
31
- clear: () => call('DELETE', '/api/shop/cart')
67
+ clear: () => call('DELETE', '/api/shop/cart'),
68
+ applyCoupon: (code) => call('POST', '/api/shop/cart/coupon', { code }),
69
+ removeCoupon: () => call('DELETE', '/api/shop/cart/coupon')
32
70
  },
33
71
  shipping: {
34
72
  list: () => call('GET', '/api/shop/shipping-methods')
@@ -5,3 +5,11 @@ export declare function createCartHandler(): {
5
5
  PATCH: RequestHandler;
6
6
  DELETE: RequestHandler;
7
7
  };
8
+ /**
9
+ * Cart coupon endpoints — POST applies a code, DELETE removes it.
10
+ * Mount at `/api/shop/cart/coupon`.
11
+ */
12
+ export declare function createCartCouponHandler(): {
13
+ POST: RequestHandler;
14
+ DELETE: RequestHandler;
15
+ };