includio-cms 0.26.0 → 0.28.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.
- package/API.md +58 -2
- package/CHANGELOG.md +105 -0
- package/DOCS.md +1 -1
- package/ROADMAP.md +8 -0
- package/dist/admin/auth-client.d.ts +42 -42
- package/dist/admin/client/admin/admin-layout.svelte +12 -2
- package/dist/admin/client/admin/admin-layout.svelte.d.ts +2 -1
- package/dist/admin/client/collection/data-table.svelte +0 -39
- package/dist/admin/client/collection/data-table.svelte.d.ts +0 -2
- package/dist/admin/client/shop/coupon-schema.d.ts +1 -1
- package/dist/admin/client/shop/refund-dialog.svelte +37 -1
- package/dist/admin/client/shop/refund-dialog.svelte.d.ts +3 -0
- package/dist/admin/client/shop/shop-order-detail-page.svelte +192 -0
- package/dist/admin/components/fields/field-renderer.svelte +6 -1
- package/dist/admin/components/fields/icon-field.svelte +86 -0
- package/dist/admin/components/fields/icon-field.svelte.d.ts +8 -0
- package/dist/admin/components/fields/icon-picker-dialog.svelte +174 -0
- package/dist/admin/components/fields/icon-picker-dialog.svelte.d.ts +11 -0
- package/dist/admin/components/fields/object-field.svelte +27 -7
- package/dist/admin/components/fields/shop-field.svelte +210 -20
- package/dist/admin/components/layout/layout-tabs.svelte +1 -0
- package/dist/admin/components/variant-form/VariantAttributeRenderer.svelte +109 -0
- package/dist/admin/components/variant-form/VariantAttributeRenderer.svelte.d.ts +9 -0
- package/dist/admin/helpers/build-icon-set-map.d.ts +8 -0
- package/dist/admin/helpers/build-icon-set-map.js +16 -0
- package/dist/admin/helpers/index.d.ts +2 -0
- package/dist/admin/helpers/index.js +2 -0
- package/dist/admin/remote/shop.remote.d.ts +116 -24
- package/dist/admin/remote/shop.remote.js +79 -6
- package/dist/admin/state/icon-sets.svelte.d.ts +9 -0
- package/dist/admin/state/icon-sets.svelte.js +20 -0
- package/dist/cli/scaffold/admin.js +2 -2
- package/dist/components/ui/checkbox/checkbox.svelte +3 -3
- package/dist/core/cms.d.ts +11 -2
- package/dist/core/cms.js +29 -0
- package/dist/core/fields/fieldSchemaToTs.js +7 -0
- package/dist/core/server/generator/fields.d.ts +2 -0
- package/dist/core/server/generator/fields.js +34 -1
- package/dist/core/server/generator/generator.js +2 -1
- package/dist/db-postgres/schema/shop/index.d.ts +1 -0
- package/dist/db-postgres/schema/shop/index.js +1 -0
- package/dist/db-postgres/schema/shop/invoice.d.ts +254 -0
- package/dist/db-postgres/schema/shop/invoice.js +27 -0
- package/dist/db-postgres/schema/shop/order.d.ts +107 -1
- package/dist/db-postgres/schema/shop/order.js +7 -1
- package/dist/db-postgres/schema/shop/payment.d.ts +20 -0
- package/dist/db-postgres/schema/shop/payment.js +4 -1
- package/dist/db-postgres/schema/shop/product.d.ts +20 -0
- package/dist/db-postgres/schema/shop/product.js +3 -1
- package/dist/db-postgres/schema/shop/productVariant.d.ts +12 -2
- package/dist/db-postgres/schema/shop/productVariant.js +22 -0
- package/dist/paraglide/messages/_index.d.ts +36 -3
- package/dist/paraglide/messages/_index.js +71 -3
- package/dist/paraglide/messages/en.d.ts +5 -0
- package/dist/paraglide/messages/en.js +14 -0
- package/dist/paraglide/messages/pl.d.ts +5 -0
- package/dist/paraglide/messages/pl.js +14 -0
- package/dist/shop/adapters/fakturownia/client.d.ts +28 -0
- package/dist/shop/adapters/fakturownia/client.js +67 -0
- package/dist/shop/adapters/fakturownia/index.d.ts +27 -0
- package/dist/shop/adapters/fakturownia/index.js +36 -0
- package/dist/shop/adapters/fakturownia/payload.d.ts +35 -0
- package/dist/shop/adapters/fakturownia/payload.js +45 -0
- package/dist/shop/cart/types.d.ts +1 -0
- package/dist/shop/client/index.d.ts +61 -0
- package/dist/shop/client/index.js +5 -1
- package/dist/shop/expiry.d.ts +35 -0
- package/dist/shop/expiry.js +68 -0
- package/dist/shop/http/balance-handler.d.ts +20 -0
- package/dist/shop/http/balance-handler.js +91 -0
- package/dist/shop/http/cart-handler.js +19 -0
- package/dist/shop/http/checkout-handler.js +30 -1
- package/dist/shop/http/index.d.ts +2 -0
- package/dist/shop/http/index.js +2 -0
- package/dist/shop/http/upcoming-handler.d.ts +16 -0
- package/dist/shop/http/upcoming-handler.js +65 -0
- package/dist/shop/http/webhook-handler.js +46 -9
- package/dist/shop/index.d.ts +7 -1
- package/dist/shop/index.js +10 -1
- package/dist/shop/nip.d.ts +12 -0
- package/dist/shop/nip.js +23 -0
- package/dist/shop/server/balance-payment.d.ts +40 -0
- package/dist/shop/server/balance-payment.js +140 -0
- package/dist/shop/server/cart-hydrate.js +2 -0
- package/dist/shop/server/init.d.ts +14 -0
- package/dist/shop/server/init.js +35 -0
- package/dist/shop/server/invoices.d.ts +64 -0
- package/dist/shop/server/invoices.js +237 -0
- package/dist/shop/server/orders.d.ts +38 -0
- package/dist/shop/server/orders.js +152 -2
- package/dist/shop/server/payment-policy.d.ts +35 -0
- package/dist/shop/server/payment-policy.js +55 -0
- package/dist/shop/server/payments.d.ts +29 -0
- package/dist/shop/server/payments.js +64 -0
- package/dist/shop/server/populate.d.ts +1 -1
- package/dist/shop/server/refund.d.ts +17 -12
- package/dist/shop/server/refund.js +96 -13
- package/dist/shop/server/shop-data.d.ts +4 -1
- package/dist/shop/server/shop-data.js +24 -2
- package/dist/shop/template.d.ts +13 -0
- package/dist/shop/template.js +98 -0
- package/dist/shop/types.d.ts +208 -1
- package/dist/shop/variant-attributes.d.ts +28 -0
- package/dist/shop/variant-attributes.js +69 -0
- package/dist/sveltekit/server/index.d.ts +1 -0
- package/dist/sveltekit/server/index.js +2 -0
- package/dist/types/cms.d.ts +4 -3
- package/dist/types/cms.schema.d.ts +1 -1
- package/dist/types/cms.schema.js +9 -0
- package/dist/types/fields.d.ts +21 -2
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.js +1 -1
- package/dist/types/plugins.d.ts +40 -0
- package/dist/types/plugins.js +4 -1
- package/dist/updates/0.26.1/index.d.ts +2 -0
- package/dist/updates/0.26.1/index.js +19 -0
- package/dist/updates/0.27.0/index.d.ts +2 -0
- package/dist/updates/0.27.0/index.js +50 -0
- package/dist/updates/0.28.0/index.d.ts +2 -0
- package/dist/updates/0.28.0/index.js +38 -0
- package/dist/updates/index.js +7 -1
- package/package.json +1 -1
- package/dist/paraglide/messages/hello_world.d.ts +0 -5
- package/dist/paraglide/messages/hello_world.js +0 -33
- package/dist/paraglide/messages/login_hello.d.ts +0 -16
- package/dist/paraglide/messages/login_hello.js +0 -34
- package/dist/paraglide/messages/login_please_login.d.ts +0 -16
- package/dist/paraglide/messages/login_please_login.js +0 -34
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { DatabaseAdapterWithDrizzle } from '../../db-postgres/index.js';
|
|
2
|
+
import type { ResolvedShopConfig } from '../types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Apply variant attribute GIN indexes for the given shop config.
|
|
5
|
+
* Idempotent (`CREATE INDEX IF NOT EXISTS`) and serialized — concurrent calls
|
|
6
|
+
* share the same in-flight promise, so a HMR re-init or fire-and-forget from
|
|
7
|
+
* `initCMS()` plus an explicit caller cannot race into a postgres
|
|
8
|
+
* `pg_class_relname_nsp_index` duplicate-key error.
|
|
9
|
+
*
|
|
10
|
+
* Both args are required to keep this module free of CMS singleton coupling
|
|
11
|
+
* (it stays importable from anywhere without circular deps).
|
|
12
|
+
* @internal
|
|
13
|
+
*/
|
|
14
|
+
export declare function applyVariantAttributeIndexes(shop: ResolvedShopConfig, db: DatabaseAdapterWithDrizzle['_drizzle']): Promise<void>;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { sql } from 'drizzle-orm';
|
|
2
|
+
import { createVariantAttributeIndexes } from '../../db-postgres/schema/shop/productVariant.js';
|
|
3
|
+
let inFlight = null;
|
|
4
|
+
/**
|
|
5
|
+
* Apply variant attribute GIN indexes for the given shop config.
|
|
6
|
+
* Idempotent (`CREATE INDEX IF NOT EXISTS`) and serialized — concurrent calls
|
|
7
|
+
* share the same in-flight promise, so a HMR re-init or fire-and-forget from
|
|
8
|
+
* `initCMS()` plus an explicit caller cannot race into a postgres
|
|
9
|
+
* `pg_class_relname_nsp_index` duplicate-key error.
|
|
10
|
+
*
|
|
11
|
+
* Both args are required to keep this module free of CMS singleton coupling
|
|
12
|
+
* (it stays importable from anywhere without circular deps).
|
|
13
|
+
* @internal
|
|
14
|
+
*/
|
|
15
|
+
export function applyVariantAttributeIndexes(shop, db) {
|
|
16
|
+
if (inFlight)
|
|
17
|
+
return inFlight;
|
|
18
|
+
// Cleanup runs via `.finally()` (microtask) — NOT inside the async body's
|
|
19
|
+
// try/finally — so the outer `inFlight = promise` assignment lands before
|
|
20
|
+
// the reset, even when the body resolves synchronously (no stmts case).
|
|
21
|
+
const promise = (async () => {
|
|
22
|
+
const stmts = createVariantAttributeIndexes(shop.variantAttributes);
|
|
23
|
+
if (stmts.length === 0)
|
|
24
|
+
return;
|
|
25
|
+
for (const stmt of stmts) {
|
|
26
|
+
await db.execute(sql.raw(stmt));
|
|
27
|
+
}
|
|
28
|
+
})();
|
|
29
|
+
inFlight = promise;
|
|
30
|
+
promise.finally(() => {
|
|
31
|
+
if (inFlight === promise)
|
|
32
|
+
inFlight = null;
|
|
33
|
+
});
|
|
34
|
+
return promise;
|
|
35
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { shopInvoicesTable, type ShopInvoiceStatus } from '../../db-postgres/schema/shop/index.js';
|
|
2
|
+
import type { Currency, InvoiceIssuePolicy, InvoicePayload, OrderStatus } from '../types.js';
|
|
3
|
+
type InvoiceRow = typeof shopInvoicesTable.$inferSelect;
|
|
4
|
+
/** Order fields the trigger / idempotency decision depends on. */
|
|
5
|
+
export interface InvoiceOrderState {
|
|
6
|
+
status: OrderStatus;
|
|
7
|
+
balanceOwed: boolean;
|
|
8
|
+
customerNip: string | null;
|
|
9
|
+
invoiceRequested: boolean;
|
|
10
|
+
}
|
|
11
|
+
/** Order fields needed to build the invoice payload. */
|
|
12
|
+
export interface InvoiceOrderData {
|
|
13
|
+
number: string;
|
|
14
|
+
currency: Currency;
|
|
15
|
+
customerEmail: string;
|
|
16
|
+
customerName: string | null;
|
|
17
|
+
customerNip: string | null;
|
|
18
|
+
customerCompanyName: string | null;
|
|
19
|
+
shippingAddress: Record<string, string> | null;
|
|
20
|
+
billingAddress: Record<string, string> | null;
|
|
21
|
+
language: string | null;
|
|
22
|
+
}
|
|
23
|
+
export interface InvoiceItemData {
|
|
24
|
+
nameSnapshot: Record<string, string>;
|
|
25
|
+
qty: number;
|
|
26
|
+
priceGrossSnapshot: number;
|
|
27
|
+
vatRate: number;
|
|
28
|
+
}
|
|
29
|
+
export type InvoiceAction = 'skip' | 'create' | 'resend';
|
|
30
|
+
export declare class InvoiceError extends Error {
|
|
31
|
+
readonly code: string;
|
|
32
|
+
constructor(code: string, message: string);
|
|
33
|
+
}
|
|
34
|
+
/** Does the order qualify for an automatic invoice under `policy`? Pure. */
|
|
35
|
+
export declare function shouldIssueInvoice(order: InvoiceOrderState, policy: InvoiceIssuePolicy): boolean;
|
|
36
|
+
/** Fully paid = a paid-or-later status with no outstanding balance. Pure. */
|
|
37
|
+
export declare function isOrderFullyPaid(order: InvoiceOrderState): boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Decide what to do with an order's invoice given any existing record. Pure —
|
|
40
|
+
* encodes the trigger + idempotency rules so they can be tested without a DB.
|
|
41
|
+
*/
|
|
42
|
+
export declare function decideInvoiceAction(order: InvoiceOrderState, existing: {
|
|
43
|
+
status: ShopInvoiceStatus;
|
|
44
|
+
} | null, policy: InvoiceIssuePolicy, opts?: {
|
|
45
|
+
force?: boolean;
|
|
46
|
+
}): InvoiceAction;
|
|
47
|
+
/** Map order + items onto the provider-agnostic {@link InvoicePayload}. Pure. */
|
|
48
|
+
export declare function buildInvoicePayload(order: InvoiceOrderData, items: InvoiceItemData[], paidAt: string): InvoicePayload;
|
|
49
|
+
export declare function getInvoiceByOrderId(orderId: string): Promise<InvoiceRow | null>;
|
|
50
|
+
/**
|
|
51
|
+
* Issue an invoice for an order if it qualifies. Fire-and-forget, fail-open —
|
|
52
|
+
* never throws, so a failing invoicing provider can't block the payment webhook.
|
|
53
|
+
* Called from `updateOrderStatus` (on `paid`) and `markBalancePaid`.
|
|
54
|
+
*/
|
|
55
|
+
export declare function maybeIssueInvoiceForOrder(orderId: string): Promise<void>;
|
|
56
|
+
/**
|
|
57
|
+
* Issue (or retry/resend) an invoice on demand from the admin. Throws on error
|
|
58
|
+
* so the caller can surface it. `force` bypasses the trigger policy and allows
|
|
59
|
+
* re-sending an already-issued invoice.
|
|
60
|
+
*/
|
|
61
|
+
export declare function issueInvoiceForOrder(orderId: string, opts?: {
|
|
62
|
+
force?: boolean;
|
|
63
|
+
}): Promise<InvoiceRow | null>;
|
|
64
|
+
export {};
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { eq } from 'drizzle-orm';
|
|
2
|
+
import { shopInvoicesTable, shopOrderItemsTable, shopOrdersTable } from '../../db-postgres/schema/shop/index.js';
|
|
3
|
+
import { getShopDb, requireShopConfig } from './db.js';
|
|
4
|
+
const FULLY_PAID_STATES = new Set(['paid', 'preparing', 'sent', 'done']);
|
|
5
|
+
export class InvoiceError extends Error {
|
|
6
|
+
code;
|
|
7
|
+
constructor(code, message) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.code = code;
|
|
10
|
+
this.name = 'InvoiceError';
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
/** Does the order qualify for an automatic invoice under `policy`? Pure. */
|
|
14
|
+
export function shouldIssueInvoice(order, policy) {
|
|
15
|
+
if (policy === 'always')
|
|
16
|
+
return true;
|
|
17
|
+
return Boolean(order.customerNip) || order.invoiceRequested;
|
|
18
|
+
}
|
|
19
|
+
/** Fully paid = a paid-or-later status with no outstanding balance. Pure. */
|
|
20
|
+
export function isOrderFullyPaid(order) {
|
|
21
|
+
return FULLY_PAID_STATES.has(order.status) && !order.balanceOwed;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Decide what to do with an order's invoice given any existing record. Pure —
|
|
25
|
+
* encodes the trigger + idempotency rules so they can be tested without a DB.
|
|
26
|
+
*/
|
|
27
|
+
export function decideInvoiceAction(order, existing, policy, opts = {}) {
|
|
28
|
+
if (!isOrderFullyPaid(order))
|
|
29
|
+
return 'skip';
|
|
30
|
+
const force = opts.force ?? false;
|
|
31
|
+
if (existing) {
|
|
32
|
+
switch (existing.status) {
|
|
33
|
+
case 'issued':
|
|
34
|
+
case 'sent':
|
|
35
|
+
return force ? 'resend' : 'skip';
|
|
36
|
+
case 'failed':
|
|
37
|
+
return 'create';
|
|
38
|
+
case 'pending':
|
|
39
|
+
return force ? 'create' : 'skip';
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (force || shouldIssueInvoice(order, policy))
|
|
43
|
+
return 'create';
|
|
44
|
+
return 'skip';
|
|
45
|
+
}
|
|
46
|
+
// Order-item `nameSnapshot` is `{ product, variant }` (see createOrderFromCart).
|
|
47
|
+
// The invoice line shows both so the customer sees the exact session they paid
|
|
48
|
+
// for (e.g. "Odporność psychiczna — Poznań • 20 września 2026").
|
|
49
|
+
function lineName(name) {
|
|
50
|
+
const product = name.product ?? '';
|
|
51
|
+
const variant = name.variant ?? '';
|
|
52
|
+
if (product && variant)
|
|
53
|
+
return `${product} — ${variant}`;
|
|
54
|
+
return product || variant || Object.values(name)[0] || '';
|
|
55
|
+
}
|
|
56
|
+
/** Map order + items onto the provider-agnostic {@link InvoicePayload}. Pure. */
|
|
57
|
+
export function buildInvoicePayload(order, items, paidAt) {
|
|
58
|
+
return {
|
|
59
|
+
orderNumber: order.number,
|
|
60
|
+
currency: order.currency,
|
|
61
|
+
paidAt,
|
|
62
|
+
buyer: {
|
|
63
|
+
name: order.customerName || order.customerEmail,
|
|
64
|
+
email: order.customerEmail,
|
|
65
|
+
nip: order.customerNip,
|
|
66
|
+
companyName: order.customerCompanyName,
|
|
67
|
+
address: order.billingAddress ?? order.shippingAddress ?? null
|
|
68
|
+
},
|
|
69
|
+
items: items.map((item) => ({
|
|
70
|
+
name: lineName(item.nameSnapshot),
|
|
71
|
+
quantity: item.qty,
|
|
72
|
+
unitPriceGross: item.priceGrossSnapshot,
|
|
73
|
+
vatRate: item.vatRate
|
|
74
|
+
}))
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
export async function getInvoiceByOrderId(orderId) {
|
|
78
|
+
const db = getShopDb();
|
|
79
|
+
const [row] = await db
|
|
80
|
+
.select()
|
|
81
|
+
.from(shopInvoicesTable)
|
|
82
|
+
.where(eq(shopInvoicesTable.orderId, orderId));
|
|
83
|
+
return row ?? null;
|
|
84
|
+
}
|
|
85
|
+
function toOrderData(order) {
|
|
86
|
+
return {
|
|
87
|
+
number: order.number,
|
|
88
|
+
currency: order.currency,
|
|
89
|
+
customerEmail: order.customerEmail,
|
|
90
|
+
customerName: order.customerName,
|
|
91
|
+
customerNip: order.customerNip,
|
|
92
|
+
customerCompanyName: order.customerCompanyName,
|
|
93
|
+
shippingAddress: order.shippingAddress ?? null,
|
|
94
|
+
billingAddress: order.billingAddress ?? null,
|
|
95
|
+
language: order.language
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
function toItemData(items) {
|
|
99
|
+
return items.map((i) => ({
|
|
100
|
+
nameSnapshot: i.nameSnapshot,
|
|
101
|
+
qty: i.qty,
|
|
102
|
+
priceGrossSnapshot: i.priceGrossSnapshot,
|
|
103
|
+
vatRate: i.vatRate
|
|
104
|
+
}));
|
|
105
|
+
}
|
|
106
|
+
async function runCreate(order, existing, adapter) {
|
|
107
|
+
const db = getShopDb();
|
|
108
|
+
const now = new Date();
|
|
109
|
+
let invoiceId;
|
|
110
|
+
if (existing) {
|
|
111
|
+
const [row] = await db
|
|
112
|
+
.update(shopInvoicesTable)
|
|
113
|
+
.set({
|
|
114
|
+
status: 'pending',
|
|
115
|
+
provider: adapter.id,
|
|
116
|
+
attempts: existing.attempts + 1,
|
|
117
|
+
lastError: null,
|
|
118
|
+
updatedAt: now
|
|
119
|
+
})
|
|
120
|
+
.where(eq(shopInvoicesTable.id, existing.id))
|
|
121
|
+
.returning();
|
|
122
|
+
invoiceId = row.id;
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
const [row] = await db
|
|
126
|
+
.insert(shopInvoicesTable)
|
|
127
|
+
.values({
|
|
128
|
+
orderId: order.id,
|
|
129
|
+
provider: adapter.id,
|
|
130
|
+
status: 'pending',
|
|
131
|
+
attempts: 1
|
|
132
|
+
})
|
|
133
|
+
.returning();
|
|
134
|
+
invoiceId = row.id;
|
|
135
|
+
}
|
|
136
|
+
const items = await db
|
|
137
|
+
.select()
|
|
138
|
+
.from(shopOrderItemsTable)
|
|
139
|
+
.where(eq(shopOrderItemsTable.orderId, order.id));
|
|
140
|
+
const paidAt = order.partialPayment?.paidAt ?? now.toISOString();
|
|
141
|
+
const payload = buildInvoicePayload(toOrderData(order), toItemData(items), paidAt);
|
|
142
|
+
try {
|
|
143
|
+
const result = await adapter.createInvoice(payload, { language: order.language });
|
|
144
|
+
let status = 'issued';
|
|
145
|
+
if (adapter.send) {
|
|
146
|
+
await adapter.send(result.externalId, { language: order.language });
|
|
147
|
+
status = 'sent';
|
|
148
|
+
}
|
|
149
|
+
const [updated] = await db
|
|
150
|
+
.update(shopInvoicesTable)
|
|
151
|
+
.set({
|
|
152
|
+
status,
|
|
153
|
+
externalId: result.externalId,
|
|
154
|
+
number: result.number ?? null,
|
|
155
|
+
pdfUrl: result.pdfUrl ?? null,
|
|
156
|
+
raw: result.raw ?? null,
|
|
157
|
+
lastError: null,
|
|
158
|
+
updatedAt: new Date()
|
|
159
|
+
})
|
|
160
|
+
.where(eq(shopInvoicesTable.id, invoiceId))
|
|
161
|
+
.returning();
|
|
162
|
+
return updated;
|
|
163
|
+
}
|
|
164
|
+
catch (err) {
|
|
165
|
+
await db
|
|
166
|
+
.update(shopInvoicesTable)
|
|
167
|
+
.set({
|
|
168
|
+
status: 'failed',
|
|
169
|
+
lastError: err instanceof Error ? err.message : String(err),
|
|
170
|
+
updatedAt: new Date()
|
|
171
|
+
})
|
|
172
|
+
.where(eq(shopInvoicesTable.id, invoiceId));
|
|
173
|
+
throw err;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
async function runResend(order, existing, adapter) {
|
|
177
|
+
if (!existing.externalId)
|
|
178
|
+
throw new InvoiceError('not_issued', 'No issued invoice to resend');
|
|
179
|
+
if (!adapter.send)
|
|
180
|
+
throw new InvoiceError('send_unsupported', 'Adapter cannot e-mail invoices');
|
|
181
|
+
const db = getShopDb();
|
|
182
|
+
await adapter.send(existing.externalId, { language: order.language });
|
|
183
|
+
const [updated] = await db
|
|
184
|
+
.update(shopInvoicesTable)
|
|
185
|
+
.set({ status: 'sent', updatedAt: new Date() })
|
|
186
|
+
.where(eq(shopInvoicesTable.id, existing.id))
|
|
187
|
+
.returning();
|
|
188
|
+
return updated;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Issue an invoice for an order if it qualifies. Fire-and-forget, fail-open —
|
|
192
|
+
* never throws, so a failing invoicing provider can't block the payment webhook.
|
|
193
|
+
* Called from `updateOrderStatus` (on `paid`) and `markBalancePaid`.
|
|
194
|
+
*/
|
|
195
|
+
export async function maybeIssueInvoiceForOrder(orderId) {
|
|
196
|
+
try {
|
|
197
|
+
const adapter = requireShopConfig().invoicing;
|
|
198
|
+
if (!adapter)
|
|
199
|
+
return;
|
|
200
|
+
const db = getShopDb();
|
|
201
|
+
const [order] = await db.select().from(shopOrdersTable).where(eq(shopOrdersTable.id, orderId));
|
|
202
|
+
if (!order)
|
|
203
|
+
return;
|
|
204
|
+
const existing = await getInvoiceByOrderId(orderId);
|
|
205
|
+
const policy = adapter.issueWhen ?? 'b2bAndOnRequest';
|
|
206
|
+
const action = decideInvoiceAction(order, existing, policy);
|
|
207
|
+
if (action === 'create')
|
|
208
|
+
await runCreate(order, existing, adapter);
|
|
209
|
+
else if (action === 'resend' && existing)
|
|
210
|
+
await runResend(order, existing, adapter);
|
|
211
|
+
}
|
|
212
|
+
catch (err) {
|
|
213
|
+
console.error(`[shop] invoice issuance failed for order ${orderId}:`, err);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Issue (or retry/resend) an invoice on demand from the admin. Throws on error
|
|
218
|
+
* so the caller can surface it. `force` bypasses the trigger policy and allows
|
|
219
|
+
* re-sending an already-issued invoice.
|
|
220
|
+
*/
|
|
221
|
+
export async function issueInvoiceForOrder(orderId, opts = {}) {
|
|
222
|
+
const adapter = requireShopConfig().invoicing;
|
|
223
|
+
if (!adapter)
|
|
224
|
+
throw new InvoiceError('no_adapter', 'No invoicing adapter configured');
|
|
225
|
+
const db = getShopDb();
|
|
226
|
+
const [order] = await db.select().from(shopOrdersTable).where(eq(shopOrdersTable.id, orderId));
|
|
227
|
+
if (!order)
|
|
228
|
+
throw new InvoiceError('order_not_found', 'Order not found');
|
|
229
|
+
const existing = await getInvoiceByOrderId(orderId);
|
|
230
|
+
const policy = adapter.issueWhen ?? 'b2bAndOnRequest';
|
|
231
|
+
const action = decideInvoiceAction(order, existing, policy, { force: opts.force ?? false });
|
|
232
|
+
if (action === 'create')
|
|
233
|
+
return runCreate(order, existing, adapter);
|
|
234
|
+
if (action === 'resend' && existing)
|
|
235
|
+
return runResend(order, existing, adapter);
|
|
236
|
+
return existing;
|
|
237
|
+
}
|
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
import { shopOrderItemsTable, shopOrderStatusHistoryTable, shopOrdersTable } from '../../db-postgres/schema/shop/index.js';
|
|
2
2
|
import type { CartItemRef } from '../cart/types.js';
|
|
3
3
|
import type { OrderStatus } from '../types.js';
|
|
4
|
+
/**
|
|
5
|
+
* @public
|
|
6
|
+
* Thrown by `createOrderFromCart` when the cart contains items from multiple
|
|
7
|
+
* products and at least one of them has a deposit `paymentPolicy`. Deposit
|
|
8
|
+
* orders must span a single product (so the balance link/refund-per-kind flow
|
|
9
|
+
* stays unambiguous). Mixed-product carts are accepted only when every
|
|
10
|
+
* involved product has a `full` (or null) policy.
|
|
11
|
+
*/
|
|
12
|
+
export declare class MixedPaymentPolicyError extends Error {
|
|
13
|
+
readonly code = "MIXED_PAYMENT_POLICY";
|
|
14
|
+
constructor(message?: string);
|
|
15
|
+
}
|
|
4
16
|
export type OrderRow = typeof shopOrdersTable.$inferSelect;
|
|
5
17
|
export type OrderItemRow = typeof shopOrderItemsTable.$inferSelect;
|
|
6
18
|
export type OrderStatusHistoryRow = typeof shopOrderStatusHistoryTable.$inferSelect;
|
|
@@ -13,7 +25,11 @@ export interface CreateOrderInput {
|
|
|
13
25
|
customerEmail: string;
|
|
14
26
|
customerName?: string;
|
|
15
27
|
customerPhone?: string;
|
|
28
|
+
customerNip?: string;
|
|
29
|
+
customerCompanyName?: string;
|
|
16
30
|
shippingAddress?: Record<string, string>;
|
|
31
|
+
billingAddress?: Record<string, string>;
|
|
32
|
+
invoiceRequested?: boolean;
|
|
17
33
|
shippingMethodId: string;
|
|
18
34
|
carrierRef?: string;
|
|
19
35
|
paymentMethod: string;
|
|
@@ -27,12 +43,34 @@ export interface CreateOrderResult {
|
|
|
27
43
|
items: OrderItemRow[];
|
|
28
44
|
requiresPaymentRedirect: boolean;
|
|
29
45
|
redirectUrl?: string;
|
|
46
|
+
/**
|
|
47
|
+
* Minor-unit amount the customer should pay at checkout under
|
|
48
|
+
* `paymentPolicy`. Equals `order.totalGross` for full-payment orders;
|
|
49
|
+
* equals the resolved deposit + shipping for deposit orders.
|
|
50
|
+
*/
|
|
51
|
+
amountToPay: number;
|
|
52
|
+
/**
|
|
53
|
+
* `'full'` for orders paid in one shot; `'deposit'` when the customer
|
|
54
|
+
* pays only a deposit at checkout and owes a balance redeemable via
|
|
55
|
+
* the balance link flow.
|
|
56
|
+
*/
|
|
57
|
+
paymentKind: 'full' | 'deposit';
|
|
30
58
|
}
|
|
31
59
|
export declare function createOrderFromCart(input: CreateOrderInput): Promise<CreateOrderResult>;
|
|
32
60
|
export declare function updateOrderStatus(orderId: string, status: OrderStatus, opts?: {
|
|
33
61
|
note?: string;
|
|
34
62
|
changedBy?: string;
|
|
63
|
+
paymentKind?: 'full' | 'deposit' | 'balance';
|
|
35
64
|
}): Promise<OrderRow>;
|
|
65
|
+
/**
|
|
66
|
+
* @internal
|
|
67
|
+
* Mark the remaining balance on a deposit order as paid. Idempotent — no-op
|
|
68
|
+
* when `balanceOwed` is already false. Updates `partialPayment.paidAmount`
|
|
69
|
+
* to the full `order.totalGross` (deposit + balance), stamps `paidAt`, and
|
|
70
|
+
* clears `balanceOwed`. Stock decrement / order status are untouched — the
|
|
71
|
+
* order already transitioned to `paid` when the deposit cleared.
|
|
72
|
+
*/
|
|
73
|
+
export declare function markBalancePaid(orderId: string): Promise<OrderRow | null>;
|
|
36
74
|
export declare function setPaymentProviderRef(orderId: string, ref: string | null): Promise<void>;
|
|
37
75
|
export interface ShipmentInfoInput {
|
|
38
76
|
shipmentId: string;
|
|
@@ -7,7 +7,25 @@ import { resolveShippingPrice } from './shipping.js';
|
|
|
7
7
|
import { generateOrderNumber } from './order-number.js';
|
|
8
8
|
import { sendLowStockEmail, sendOrderStatusEmail } from './email.js';
|
|
9
9
|
import { isPaymentMethodAllowed } from './payment-compat.js';
|
|
10
|
+
import { isVariantExpired, VariantExpiredError } from '../expiry.js';
|
|
11
|
+
import { resolvePaymentAmount } from './payment-policy.js';
|
|
12
|
+
import { maybeIssueInvoiceForOrder } from './invoices.js';
|
|
10
13
|
import { CouponError, recordCouponRedemption, releaseCouponSlot, reserveCouponSlot, validateCoupon } from './coupons.js';
|
|
14
|
+
/**
|
|
15
|
+
* @public
|
|
16
|
+
* Thrown by `createOrderFromCart` when the cart contains items from multiple
|
|
17
|
+
* products and at least one of them has a deposit `paymentPolicy`. Deposit
|
|
18
|
+
* orders must span a single product (so the balance link/refund-per-kind flow
|
|
19
|
+
* stays unambiguous). Mixed-product carts are accepted only when every
|
|
20
|
+
* involved product has a `full` (or null) policy.
|
|
21
|
+
*/
|
|
22
|
+
export class MixedPaymentPolicyError extends Error {
|
|
23
|
+
code = 'MIXED_PAYMENT_POLICY';
|
|
24
|
+
constructor(message = 'Cart mixes a deposit-policy product with other products.') {
|
|
25
|
+
super(message);
|
|
26
|
+
this.name = 'MixedPaymentPolicyError';
|
|
27
|
+
}
|
|
28
|
+
}
|
|
11
29
|
const STOCK_RESERVATION_TTL_MINUTES = 30;
|
|
12
30
|
async function purgeExpiredReservations() {
|
|
13
31
|
const db = getShopDb();
|
|
@@ -97,6 +115,43 @@ export async function createOrderFromCart(input) {
|
|
|
97
115
|
discountAmount: snapshot.subtotalGross - snapshot.totalGross
|
|
98
116
|
};
|
|
99
117
|
}
|
|
118
|
+
// Payment policy lookup + mixed-cart guard. Load each line's product
|
|
119
|
+
// paymentPolicy from shop_products. If any product has a deposit policy
|
|
120
|
+
// and the cart spans more than one product, refuse with
|
|
121
|
+
// MIXED_PAYMENT_POLICY (keeps balance link / refund-per-kind unambiguous).
|
|
122
|
+
const uniqueProductIds = Array.from(new Set(snapshot.items.map((l) => l.productId).filter(Boolean)));
|
|
123
|
+
const productPolicyRows = uniqueProductIds.length > 0
|
|
124
|
+
? await db
|
|
125
|
+
.select({
|
|
126
|
+
id: shopProductsTable.id,
|
|
127
|
+
paymentPolicy: shopProductsTable.paymentPolicy
|
|
128
|
+
})
|
|
129
|
+
.from(shopProductsTable)
|
|
130
|
+
.where(inArray(shopProductsTable.id, uniqueProductIds))
|
|
131
|
+
: [];
|
|
132
|
+
const policyByProductId = new Map(productPolicyRows.map((r) => [r.id, r.paymentPolicy]));
|
|
133
|
+
const hasDepositPolicy = productPolicyRows.some((r) => r.paymentPolicy?.type === 'deposit');
|
|
134
|
+
if (hasDepositPolicy && uniqueProductIds.length > 1) {
|
|
135
|
+
throw new MixedPaymentPolicyError();
|
|
136
|
+
}
|
|
137
|
+
// Variant expiry guard (opt-in via defineShop.variantExpiry). Run before
|
|
138
|
+
// stock reservation so expired variants surface a domain error instead of
|
|
139
|
+
// taking a reservation slot they will never use.
|
|
140
|
+
if (shop.variantExpiry) {
|
|
141
|
+
const variantIds = snapshot.items.map((l) => l.variantId);
|
|
142
|
+
const variantRows = await db
|
|
143
|
+
.select({
|
|
144
|
+
id: shopProductVariantsTable.id,
|
|
145
|
+
attributes: shopProductVariantsTable.attributes
|
|
146
|
+
})
|
|
147
|
+
.from(shopProductVariantsTable)
|
|
148
|
+
.where(inArray(shopProductVariantsTable.id, variantIds));
|
|
149
|
+
for (const v of variantRows) {
|
|
150
|
+
if (isVariantExpired({ attributes: v.attributes }, shop.variantExpiry)) {
|
|
151
|
+
throw new VariantExpiredError(v.id);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
100
155
|
// Stock availability under active reservations (when stock feature on)
|
|
101
156
|
if (shop.features.stock) {
|
|
102
157
|
await purgeExpiredReservations();
|
|
@@ -120,6 +175,30 @@ export async function createOrderFromCart(input) {
|
|
|
120
175
|
const totalNet = snapshot.totalNet + shippingResolved.net;
|
|
121
176
|
const totalGross = snapshot.totalGross + shippingResolved.gross;
|
|
122
177
|
const totalVat = snapshot.totalVat + shippingResolved.vat;
|
|
178
|
+
// Resolve per-line payment amounts under each product's paymentPolicy.
|
|
179
|
+
// `depositSum` is what we charge at checkout under deposit policy; the
|
|
180
|
+
// remainder + shipping rolls into `partialPayment.balanceAmount`.
|
|
181
|
+
let depositSum = 0;
|
|
182
|
+
let goodsBalance = 0;
|
|
183
|
+
let anyDepositLine = false;
|
|
184
|
+
for (const line of snapshot.items) {
|
|
185
|
+
const policy = policyByProductId.get(line.productId) ?? null;
|
|
186
|
+
const resolved = resolvePaymentAmount(policy, line.lineGross);
|
|
187
|
+
depositSum += resolved.amountToPay;
|
|
188
|
+
goodsBalance += resolved.balanceAmount;
|
|
189
|
+
if (resolved.kind === 'deposit')
|
|
190
|
+
anyDepositLine = true;
|
|
191
|
+
}
|
|
192
|
+
const paymentKind = anyDepositLine ? 'deposit' : 'full';
|
|
193
|
+
const amountToPay = anyDepositLine ? depositSum + shippingResolved.gross : totalGross;
|
|
194
|
+
const partialPayment = anyDepositLine
|
|
195
|
+
? {
|
|
196
|
+
kind: 'deposit',
|
|
197
|
+
paidAmount: 0,
|
|
198
|
+
balanceAmount: goodsBalance,
|
|
199
|
+
paidAt: null
|
|
200
|
+
}
|
|
201
|
+
: null;
|
|
123
202
|
// Consents snapshot
|
|
124
203
|
const consentSnapshot = shop.consents.map((c) => {
|
|
125
204
|
const accepted = input.consents?.find((x) => x.id === c.id)?.accepted ?? false;
|
|
@@ -159,7 +238,11 @@ export async function createOrderFromCart(input) {
|
|
|
159
238
|
customerEmail: input.customerEmail,
|
|
160
239
|
customerName: input.customerName ?? null,
|
|
161
240
|
customerPhone: input.customerPhone ?? null,
|
|
241
|
+
customerNip: input.customerNip ?? null,
|
|
242
|
+
customerCompanyName: input.customerCompanyName ?? null,
|
|
162
243
|
shippingAddress: input.shippingAddress ?? null,
|
|
244
|
+
billingAddress: input.billingAddress ?? null,
|
|
245
|
+
invoiceRequested: input.invoiceRequested ?? false,
|
|
163
246
|
totalNet,
|
|
164
247
|
totalGross,
|
|
165
248
|
vatAmount: totalVat,
|
|
@@ -171,7 +254,9 @@ export async function createOrderFromCart(input) {
|
|
|
171
254
|
paymentMethod: input.paymentMethod,
|
|
172
255
|
consents: consentSnapshot,
|
|
173
256
|
notes: input.notes ?? null,
|
|
174
|
-
language: input.language ?? cms.languages[0] ?? null
|
|
257
|
+
language: input.language ?? cms.languages[0] ?? null,
|
|
258
|
+
partialPayment,
|
|
259
|
+
balanceOwed: false
|
|
175
260
|
})
|
|
176
261
|
.returning();
|
|
177
262
|
order = inserted;
|
|
@@ -237,7 +322,9 @@ export async function createOrderFromCart(input) {
|
|
|
237
322
|
return {
|
|
238
323
|
order,
|
|
239
324
|
items,
|
|
240
|
-
requiresPaymentRedirect: false
|
|
325
|
+
requiresPaymentRedirect: false,
|
|
326
|
+
amountToPay,
|
|
327
|
+
paymentKind
|
|
241
328
|
};
|
|
242
329
|
}
|
|
243
330
|
export async function updateOrderStatus(orderId, status, opts = {}) {
|
|
@@ -248,6 +335,32 @@ export async function updateOrderStatus(orderId, status, opts = {}) {
|
|
|
248
335
|
if (order.status === status)
|
|
249
336
|
return order;
|
|
250
337
|
const shop = requireShopConfig();
|
|
338
|
+
// Deposit-kind transition to `paid`: bump partialPayment.paidAmount to the
|
|
339
|
+
// deposit amount, stamp paidAt, and flag balanceOwed = true. Stock is
|
|
340
|
+
// permanently confirmed by the existing `paid` branch below — no TTL
|
|
341
|
+
// release. Balance-kind transitions skip the order-level status change
|
|
342
|
+
// (handled separately by markBalancePaid) so we never reach this branch.
|
|
343
|
+
//
|
|
344
|
+
// Auto-detect deposit context when caller (e.g. admin manual mark-paid)
|
|
345
|
+
// didn't pass paymentKind: a non-balance-owed deposit order with paidAt
|
|
346
|
+
// still null = the initial deposit payment is the one being confirmed.
|
|
347
|
+
const partial = order.partialPayment;
|
|
348
|
+
const effectiveKind = opts.paymentKind ??
|
|
349
|
+
(partial?.kind === 'deposit' && !order.balanceOwed && partial.paidAt == null
|
|
350
|
+
? 'deposit'
|
|
351
|
+
: undefined);
|
|
352
|
+
if (status === 'paid' && effectiveKind === 'deposit' && order.partialPayment) {
|
|
353
|
+
const pp = order.partialPayment;
|
|
354
|
+
const paidAt = new Date().toISOString();
|
|
355
|
+
const paidAmount = order.totalGross - pp.balanceAmount;
|
|
356
|
+
await db
|
|
357
|
+
.update(shopOrdersTable)
|
|
358
|
+
.set({
|
|
359
|
+
partialPayment: { ...pp, paidAmount, paidAt },
|
|
360
|
+
balanceOwed: true
|
|
361
|
+
})
|
|
362
|
+
.where(eq(shopOrdersTable.id, orderId));
|
|
363
|
+
}
|
|
251
364
|
await db
|
|
252
365
|
.update(shopOrdersTable)
|
|
253
366
|
.set({ status, updatedAt: new Date() })
|
|
@@ -322,8 +435,45 @@ export async function updateOrderStatus(orderId, status, opts = {}) {
|
|
|
322
435
|
}
|
|
323
436
|
const [updated] = await db.select().from(shopOrdersTable).where(eq(shopOrdersTable.id, orderId));
|
|
324
437
|
void sendOrderStatusEmail(orderId, status);
|
|
438
|
+
// Auto-issue an invoice once the order is paid. Fail-open / fire-and-forget —
|
|
439
|
+
// the guard inside skips deposit orders that still owe a balance.
|
|
440
|
+
if (status === 'paid')
|
|
441
|
+
void maybeIssueInvoiceForOrder(orderId);
|
|
325
442
|
return updated;
|
|
326
443
|
}
|
|
444
|
+
/**
|
|
445
|
+
* @internal
|
|
446
|
+
* Mark the remaining balance on a deposit order as paid. Idempotent — no-op
|
|
447
|
+
* when `balanceOwed` is already false. Updates `partialPayment.paidAmount`
|
|
448
|
+
* to the full `order.totalGross` (deposit + balance), stamps `paidAt`, and
|
|
449
|
+
* clears `balanceOwed`. Stock decrement / order status are untouched — the
|
|
450
|
+
* order already transitioned to `paid` when the deposit cleared.
|
|
451
|
+
*/
|
|
452
|
+
export async function markBalancePaid(orderId) {
|
|
453
|
+
const db = getShopDb();
|
|
454
|
+
const [order] = await db.select().from(shopOrdersTable).where(eq(shopOrdersTable.id, orderId));
|
|
455
|
+
if (!order)
|
|
456
|
+
return null;
|
|
457
|
+
if (!order.balanceOwed || !order.partialPayment)
|
|
458
|
+
return order;
|
|
459
|
+
const pp = order.partialPayment;
|
|
460
|
+
await db
|
|
461
|
+
.update(shopOrdersTable)
|
|
462
|
+
.set({
|
|
463
|
+
partialPayment: {
|
|
464
|
+
...pp,
|
|
465
|
+
paidAmount: order.totalGross,
|
|
466
|
+
paidAt: new Date().toISOString()
|
|
467
|
+
},
|
|
468
|
+
balanceOwed: false,
|
|
469
|
+
updatedAt: new Date()
|
|
470
|
+
})
|
|
471
|
+
.where(eq(shopOrdersTable.id, orderId));
|
|
472
|
+
const [updated] = await db.select().from(shopOrdersTable).where(eq(shopOrdersTable.id, orderId));
|
|
473
|
+
// Balance cleared → the order is now fully paid; issue its invoice.
|
|
474
|
+
void maybeIssueInvoiceForOrder(orderId);
|
|
475
|
+
return updated ?? null;
|
|
476
|
+
}
|
|
327
477
|
export async function setPaymentProviderRef(orderId, ref) {
|
|
328
478
|
const db = getShopDb();
|
|
329
479
|
await db
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { DepositAmount, PaymentPolicy } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* @public
|
|
4
|
+
* Result of resolving a per-line payment amount under a `PaymentPolicy`.
|
|
5
|
+
* `amountToPay` is the minor-unit amount to charge now; `balanceAmount` is
|
|
6
|
+
* what will still be owed under deposit policy (0 for full / clamped deposit).
|
|
7
|
+
*/
|
|
8
|
+
export interface ResolvedPaymentAmount {
|
|
9
|
+
amountToPay: number;
|
|
10
|
+
kind: 'full' | 'deposit';
|
|
11
|
+
balanceAmount: number;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* @internal
|
|
15
|
+
* Compute the deposit minor-unit amount from a `DepositAmount` spec. Percent
|
|
16
|
+
* floors `(base * value / 100)`; fixed amount is clamped to `base` to prevent
|
|
17
|
+
* collecting more than the line total. Caller is responsible for validating
|
|
18
|
+
* the spec (see {@link validatePaymentPolicy}).
|
|
19
|
+
*/
|
|
20
|
+
export declare function computeDepositAmount(spec: DepositAmount, base: number): number;
|
|
21
|
+
/**
|
|
22
|
+
* @public
|
|
23
|
+
* Resolve a `PaymentPolicy` against a line gross total. Returns the immediate
|
|
24
|
+
* `amountToPay` (deposit when applicable), the `kind` for payment row tagging
|
|
25
|
+
* (`'full' | 'deposit'`), and the `balanceAmount` still owed. Null / `full`
|
|
26
|
+
* policies short-circuit to a normal full charge with `balanceAmount = 0`.
|
|
27
|
+
*/
|
|
28
|
+
export declare function resolvePaymentAmount(policy: PaymentPolicy | null, lineGross: number): ResolvedPaymentAmount;
|
|
29
|
+
/**
|
|
30
|
+
* @internal
|
|
31
|
+
* Validate a `PaymentPolicy` configuration. Throws a `RangeError` when the
|
|
32
|
+
* spec is structurally invalid (percent out of (0, 100], amount ≤ 0, etc).
|
|
33
|
+
* Called from `upsertShopData` before persisting the policy on the product.
|
|
34
|
+
*/
|
|
35
|
+
export declare function validatePaymentPolicy(policy: PaymentPolicy): void;
|