includio-cms 0.27.0 → 0.33.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 -14
- package/CHANGELOG.md +59 -0
- package/DOCS.md +1 -1
- package/ROADMAP.md +1 -0
- package/dist/admin/api/handler.js +4 -0
- package/dist/admin/api/integrations.d.ts +13 -0
- package/dist/admin/api/integrations.js +61 -0
- package/dist/admin/api/test-email.d.ts +9 -0
- package/dist/admin/api/test-email.js +39 -0
- package/dist/admin/auth-client.d.ts +543 -543
- package/dist/admin/client/index.d.ts +10 -0
- package/dist/admin/client/index.js +12 -0
- package/dist/admin/client/maintenance/maintenance-page.svelte +210 -0
- package/dist/admin/client/shop/coupon-schema.d.ts +1 -1
- package/dist/admin/client/shop/restore-order-cell.svelte +29 -0
- package/dist/admin/client/shop/restore-order-cell.svelte.d.ts +8 -0
- package/dist/admin/client/shop/shop-order-detail-page.svelte +156 -1
- package/dist/admin/client/shop/shop-orders-list-page.svelte +113 -53
- package/dist/admin/components/layout/app-sidebar.svelte +2 -0
- package/dist/admin/components/layout/nav-custom.svelte +26 -0
- package/dist/admin/components/layout/nav-custom.svelte.d.ts +3 -0
- package/dist/admin/components/layout/page-header.svelte +13 -3
- package/dist/admin/components/layout/page-header.svelte.d.ts +13 -3
- package/dist/admin/remote/admin.remote.d.ts +7 -0
- package/dist/admin/remote/admin.remote.js +10 -0
- package/dist/admin/remote/entry.remote.d.ts +2 -2
- package/dist/admin/remote/index.d.ts +1 -0
- package/dist/admin/remote/index.js +1 -0
- package/dist/admin/remote/invite.d.ts +1 -1
- package/dist/admin/remote/shop.remote.d.ts +125 -40
- package/dist/admin/remote/shop.remote.js +59 -10
- package/dist/admin/types.d.ts +15 -0
- package/dist/admin/utils/csv-export.d.ts +45 -0
- package/dist/admin/utils/csv-export.js +61 -0
- package/dist/cli/scaffold/admin.js +1 -1
- package/dist/components/ui/input/input.svelte.d.ts +1 -1
- package/dist/components/ui/input-group/input-group-input.svelte.d.ts +1 -1
- package/dist/components/ui/sidebar/sidebar-input.svelte.d.ts +1 -1
- package/dist/core/cms.d.ts +44 -2
- package/dist/core/cms.js +64 -0
- package/dist/core/index.d.ts +2 -4
- package/dist/core/index.js +1 -4
- package/dist/core/server/index.d.ts +4 -1
- package/dist/core/server/index.js +4 -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 +104 -0
- package/dist/db-postgres/schema/shop/order.js +8 -0
- package/dist/shop/adapters/fakturownia/client.d.ts +33 -0
- package/dist/shop/adapters/fakturownia/client.js +87 -0
- package/dist/shop/adapters/fakturownia/index.d.ts +27 -0
- package/dist/shop/adapters/fakturownia/index.js +47 -0
- package/dist/shop/adapters/fakturownia/payload.d.ts +35 -0
- package/dist/shop/adapters/fakturownia/payload.js +45 -0
- package/dist/shop/adapters/payu/index.js +11 -0
- package/dist/shop/client/index.d.ts +7 -0
- package/dist/shop/http/checkout-handler.js +11 -0
- package/dist/shop/index.d.ts +4 -1
- package/dist/shop/index.js +3 -0
- package/dist/shop/nip.d.ts +12 -0
- package/dist/shop/nip.js +23 -0
- package/dist/shop/server/coupons.d.ts +10 -0
- package/dist/shop/server/coupons.js +19 -0
- package/dist/shop/server/email.d.ts +7 -3
- package/dist/shop/server/email.js +86 -112
- package/dist/shop/server/emailTemplateRegistry.d.ts +47 -0
- package/dist/shop/server/emailTemplateRegistry.js +288 -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 +64 -1
- package/dist/shop/server/orders.js +155 -15
- package/dist/shop/templates/_partials/footer.en.html +4 -0
- package/dist/shop/templates/_partials/footer.pl.html +4 -0
- package/dist/shop/templates/_partials/header.en.html +4 -0
- package/dist/shop/templates/_partials/header.pl.html +4 -0
- package/dist/shop/templates/_partials/items.en.html +14 -0
- package/dist/shop/templates/_partials/items.pl.html +14 -0
- package/dist/shop/templates/_partials/tracking.en.html +7 -0
- package/dist/shop/templates/_partials/tracking.pl.html +7 -0
- package/dist/shop/templates/awaiting-payment.en.html +6 -0
- package/dist/shop/templates/awaiting-payment.pl.html +6 -0
- package/dist/shop/templates/cancelled.en.html +6 -0
- package/dist/shop/templates/cancelled.pl.html +6 -0
- package/dist/shop/templates/low-stock.en.html +14 -0
- package/dist/shop/templates/low-stock.pl.html +14 -0
- package/dist/shop/templates/order-completed.en.html +6 -0
- package/dist/shop/templates/order-completed.pl.html +6 -0
- package/dist/shop/templates/order-received.en.html +7 -0
- package/dist/shop/templates/order-received.pl.html +7 -0
- package/dist/shop/templates/payment-received.en.html +7 -0
- package/dist/shop/templates/payment-received.pl.html +7 -0
- package/dist/shop/templates/payment-rejected.en.html +6 -0
- package/dist/shop/templates/payment-rejected.pl.html +6 -0
- package/dist/shop/templates/preparing.en.html +7 -0
- package/dist/shop/templates/preparing.pl.html +7 -0
- package/dist/shop/templates/refunded.en.html +6 -0
- package/dist/shop/templates/refunded.pl.html +6 -0
- package/dist/shop/templates/shipped.en.html +7 -0
- package/dist/shop/templates/shipped.pl.html +7 -0
- package/dist/shop/types.d.ts +130 -1
- package/dist/sveltekit/index.d.ts +0 -1
- package/dist/sveltekit/index.js +0 -1
- package/dist/sveltekit/server/index.d.ts +1 -0
- package/dist/sveltekit/server/index.js +1 -0
- package/dist/types/adapters/email.d.ts +13 -0
- package/dist/types/cms.d.ts +30 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/updates/0.28.0/index.d.ts +2 -0
- package/dist/updates/0.28.0/index.js +38 -0
- package/dist/updates/0.34.0/index.d.ts +2 -0
- package/dist/updates/0.34.0/index.js +17 -0
- package/dist/updates/index.js +5 -1
- package/package.json +7 -2
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
export class FakturowniaApiError extends Error {
|
|
2
|
+
status;
|
|
3
|
+
body;
|
|
4
|
+
constructor(message, status, body) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.status = status;
|
|
7
|
+
this.body = body;
|
|
8
|
+
this.name = 'FakturowniaApiError';
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export class FakturowniaClient {
|
|
12
|
+
base;
|
|
13
|
+
apiToken;
|
|
14
|
+
fetchFn;
|
|
15
|
+
constructor(opts) {
|
|
16
|
+
if (!opts.domain)
|
|
17
|
+
throw new Error('FakturowniaClient: domain required.');
|
|
18
|
+
if (!opts.apiToken)
|
|
19
|
+
throw new Error('FakturowniaClient: apiToken required.');
|
|
20
|
+
// Accept a bare subdomain (`acme`) as well as a full host or URL
|
|
21
|
+
// (`acme.fakturownia.pl`, `https://acme.fakturownia.pl/`) — strip protocol,
|
|
22
|
+
// the `.fakturownia.pl` suffix and trailing slashes so we never double it.
|
|
23
|
+
const subdomain = opts.domain
|
|
24
|
+
.trim()
|
|
25
|
+
.replace(/^https?:\/\//i, '')
|
|
26
|
+
.replace(/\/+$/, '')
|
|
27
|
+
.replace(/\.fakturownia\.pl$/i, '');
|
|
28
|
+
this.base = `https://${subdomain}.fakturownia.pl`;
|
|
29
|
+
this.apiToken = opts.apiToken;
|
|
30
|
+
this.fetchFn = opts.fetch ?? globalThis.fetch.bind(globalThis);
|
|
31
|
+
}
|
|
32
|
+
async createInvoice(invoice) {
|
|
33
|
+
return this.post('/invoices.json', {
|
|
34
|
+
api_token: this.apiToken,
|
|
35
|
+
invoice
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
async sendByEmail(id) {
|
|
39
|
+
await this.postRaw(`/invoices/${encodeURIComponent(String(id))}/send_by_email.json`, {
|
|
40
|
+
api_token: this.apiToken
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Read-only account lookup — used for connectivity/auth health checks.
|
|
45
|
+
* Does NOT create or modify anything. A 200 means domain + token are valid.
|
|
46
|
+
*/
|
|
47
|
+
async getAccount() {
|
|
48
|
+
const res = await this.fetchFn(`${this.base}/account.json?api_token=${encodeURIComponent(this.apiToken)}`, { headers: { Accept: 'application/json' } });
|
|
49
|
+
if (!res.ok) {
|
|
50
|
+
let body;
|
|
51
|
+
try {
|
|
52
|
+
body = await res.json();
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
body = await res.text().catch(() => undefined);
|
|
56
|
+
}
|
|
57
|
+
const raw = body == null ? '' : typeof body === 'string' ? body : JSON.stringify(body);
|
|
58
|
+
const detail = raw ? `: ${raw.slice(0, 400)}` : '';
|
|
59
|
+
throw new FakturowniaApiError(`Fakturownia /account.json → ${res.status}${detail}`, res.status, body);
|
|
60
|
+
}
|
|
61
|
+
return res.json();
|
|
62
|
+
}
|
|
63
|
+
async postRaw(path, payload) {
|
|
64
|
+
const res = await this.fetchFn(`${this.base}${path}`, {
|
|
65
|
+
method: 'POST',
|
|
66
|
+
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
|
67
|
+
body: JSON.stringify(payload)
|
|
68
|
+
});
|
|
69
|
+
if (!res.ok) {
|
|
70
|
+
let body;
|
|
71
|
+
try {
|
|
72
|
+
body = await res.json();
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
body = await res.text().catch(() => undefined);
|
|
76
|
+
}
|
|
77
|
+
const raw = body == null ? '' : typeof body === 'string' ? body : JSON.stringify(body);
|
|
78
|
+
const detail = raw ? `: ${raw.slice(0, 400)}` : '';
|
|
79
|
+
throw new FakturowniaApiError(`Fakturownia ${path} → ${res.status}${detail}`, res.status, body);
|
|
80
|
+
}
|
|
81
|
+
return res;
|
|
82
|
+
}
|
|
83
|
+
async post(path, payload) {
|
|
84
|
+
const res = await this.postRaw(path, payload);
|
|
85
|
+
return (await res.json());
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { InvoiceIssuePolicy, InvoicingAdapter } from '../../types.js';
|
|
2
|
+
export interface FakturowniaAdapterOptions {
|
|
3
|
+
/** Account subdomain (`acme`) or full host (`acme.fakturownia.pl`). Required. */
|
|
4
|
+
domain: string;
|
|
5
|
+
/** Fakturownia API token. Required. Pass from `$env/dynamic/private`. */
|
|
6
|
+
apiToken: string;
|
|
7
|
+
/** Adapter id. Default `fakturownia`. */
|
|
8
|
+
id?: string;
|
|
9
|
+
/** Invoice kind sent to Fakturownia. Default `vat`. */
|
|
10
|
+
kind?: string;
|
|
11
|
+
/** Unit of measure per line — KSeF list: szt, godz, dni, mc, m2, kg. Default `szt`. */
|
|
12
|
+
unit?: string;
|
|
13
|
+
/** When to issue automatically. Default (server-side) `b2bAndOnRequest`. */
|
|
14
|
+
issueWhen?: InvoiceIssuePolicy;
|
|
15
|
+
/** E-mail the invoice to the buyer via Fakturownia. Default `true`. */
|
|
16
|
+
sendEmail?: boolean;
|
|
17
|
+
/** Override fetch — primarily for testing. */
|
|
18
|
+
fetch?: typeof fetch;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Invoicing adapter backed by Fakturownia (fakturownia.pl). Issues a paid VAT
|
|
22
|
+
* invoice from a fully-paid order and (by default) e-mails the PDF to the buyer
|
|
23
|
+
* provider-side. Seller data and numbering are managed in the Fakturownia
|
|
24
|
+
* account; the NIP is validated upstream at checkout.
|
|
25
|
+
* @public
|
|
26
|
+
*/
|
|
27
|
+
export declare function fakturowniaAdapter(opts: FakturowniaAdapterOptions): InvoicingAdapter;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { FakturowniaClient } from './client.js';
|
|
2
|
+
import { buildFakturowniaInvoice } from './payload.js';
|
|
3
|
+
/**
|
|
4
|
+
* Invoicing adapter backed by Fakturownia (fakturownia.pl). Issues a paid VAT
|
|
5
|
+
* invoice from a fully-paid order and (by default) e-mails the PDF to the buyer
|
|
6
|
+
* provider-side. Seller data and numbering are managed in the Fakturownia
|
|
7
|
+
* account; the NIP is validated upstream at checkout.
|
|
8
|
+
* @public
|
|
9
|
+
*/
|
|
10
|
+
export function fakturowniaAdapter(opts) {
|
|
11
|
+
const client = new FakturowniaClient({
|
|
12
|
+
domain: opts.domain,
|
|
13
|
+
apiToken: opts.apiToken,
|
|
14
|
+
fetch: opts.fetch
|
|
15
|
+
});
|
|
16
|
+
const adapter = {
|
|
17
|
+
id: opts.id ?? 'fakturownia',
|
|
18
|
+
issueWhen: opts.issueWhen,
|
|
19
|
+
async createInvoice(payload) {
|
|
20
|
+
const body = buildFakturowniaInvoice(payload, { kind: opts.kind, unit: opts.unit });
|
|
21
|
+
const inv = await client.createInvoice(body);
|
|
22
|
+
return {
|
|
23
|
+
externalId: String(inv.id),
|
|
24
|
+
number: inv.number,
|
|
25
|
+
pdfUrl: inv.view_url,
|
|
26
|
+
raw: inv
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
if (opts.sendEmail !== false) {
|
|
31
|
+
adapter.send = async (externalId, _ctx) => {
|
|
32
|
+
await client.sendByEmail(externalId);
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
adapter.healthCheck = async () => {
|
|
36
|
+
// Read-only /account.json lookup — verifies domain + token without
|
|
37
|
+
// issuing an invoice.
|
|
38
|
+
try {
|
|
39
|
+
await client.getAccount();
|
|
40
|
+
return { ok: true };
|
|
41
|
+
}
|
|
42
|
+
catch (e) {
|
|
43
|
+
return { ok: false, message: e instanceof Error ? e.message : String(e) };
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
return adapter;
|
|
47
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { InvoicePayload } from '../../types.js';
|
|
2
|
+
export interface FakturowniaPosition {
|
|
3
|
+
name: string;
|
|
4
|
+
quantity: number;
|
|
5
|
+
/** Total gross price of the line (unit × quantity) in the major unit (PLN). */
|
|
6
|
+
total_price_gross: number;
|
|
7
|
+
/** VAT rate as a plain percentage (e.g. 23). */
|
|
8
|
+
tax: number;
|
|
9
|
+
/** Unit of measure — KSeF requires one of: szt, godz, dni, mc, m2, kg. */
|
|
10
|
+
quantity_unit: string;
|
|
11
|
+
}
|
|
12
|
+
export interface FakturowniaInvoiceBody {
|
|
13
|
+
kind: string;
|
|
14
|
+
status: 'paid';
|
|
15
|
+
issue_date: string;
|
|
16
|
+
sell_date: string;
|
|
17
|
+
paid_date: string;
|
|
18
|
+
currency: string;
|
|
19
|
+
buyer_name: string;
|
|
20
|
+
buyer_email: string;
|
|
21
|
+
buyer_tax_no?: string;
|
|
22
|
+
buyer_street?: string;
|
|
23
|
+
buyer_city?: string;
|
|
24
|
+
buyer_post_code?: string;
|
|
25
|
+
buyer_country?: string;
|
|
26
|
+
positions: FakturowniaPosition[];
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Map an {@link InvoicePayload} onto the Fakturownia `invoice` object. Pure —
|
|
30
|
+
* no network. The NIP is assumed already validated upstream (checkout).
|
|
31
|
+
*/
|
|
32
|
+
export declare function buildFakturowniaInvoice(payload: InvoicePayload, opts?: {
|
|
33
|
+
kind?: string;
|
|
34
|
+
unit?: string;
|
|
35
|
+
}): FakturowniaInvoiceBody;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/** Minor units (grosze) → major units (PLN) with 2-decimal precision. */
|
|
2
|
+
function toMajor(minor) {
|
|
3
|
+
return Math.round(minor) / 100;
|
|
4
|
+
}
|
|
5
|
+
function pick(addr, ...keys) {
|
|
6
|
+
if (!addr)
|
|
7
|
+
return undefined;
|
|
8
|
+
for (const k of keys) {
|
|
9
|
+
if (addr[k])
|
|
10
|
+
return addr[k];
|
|
11
|
+
}
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Map an {@link InvoicePayload} onto the Fakturownia `invoice` object. Pure —
|
|
16
|
+
* no network. The NIP is assumed already validated upstream (checkout).
|
|
17
|
+
*/
|
|
18
|
+
export function buildFakturowniaInvoice(payload, opts = {}) {
|
|
19
|
+
const date = payload.paidAt.slice(0, 10);
|
|
20
|
+
const { buyer } = payload;
|
|
21
|
+
return {
|
|
22
|
+
kind: opts.kind ?? 'vat',
|
|
23
|
+
status: 'paid',
|
|
24
|
+
issue_date: date,
|
|
25
|
+
sell_date: date,
|
|
26
|
+
paid_date: date,
|
|
27
|
+
currency: payload.currency,
|
|
28
|
+
buyer_name: buyer.companyName || buyer.name,
|
|
29
|
+
buyer_email: buyer.email,
|
|
30
|
+
...(buyer.nip ? { buyer_tax_no: buyer.nip } : {}),
|
|
31
|
+
...(pick(buyer.address, 'street') ? { buyer_street: pick(buyer.address, 'street') } : {}),
|
|
32
|
+
...(pick(buyer.address, 'city') ? { buyer_city: pick(buyer.address, 'city') } : {}),
|
|
33
|
+
...(pick(buyer.address, 'postCode', 'zip', 'postalCode')
|
|
34
|
+
? { buyer_post_code: pick(buyer.address, 'postCode', 'zip', 'postalCode') }
|
|
35
|
+
: {}),
|
|
36
|
+
...(pick(buyer.address, 'country') ? { buyer_country: pick(buyer.address, 'country') } : {}),
|
|
37
|
+
positions: payload.items.map((item) => ({
|
|
38
|
+
name: item.name,
|
|
39
|
+
quantity: item.quantity,
|
|
40
|
+
total_price_gross: toMajor(item.unitPriceGross * item.quantity),
|
|
41
|
+
tax: item.vatRate,
|
|
42
|
+
quantity_unit: opts.unit ?? 'szt'
|
|
43
|
+
}))
|
|
44
|
+
};
|
|
45
|
+
}
|
|
@@ -99,6 +99,17 @@ export function payuAdapter(opts) {
|
|
|
99
99
|
amount: input.amount ?? 0,
|
|
100
100
|
raw: result.raw
|
|
101
101
|
};
|
|
102
|
+
},
|
|
103
|
+
async healthCheck() {
|
|
104
|
+
// OAuth client_credentials token fetch — reaches PayU and verifies
|
|
105
|
+
// posId/clientId/clientSecret without creating an order.
|
|
106
|
+
try {
|
|
107
|
+
await client.getAccessToken();
|
|
108
|
+
return { ok: true };
|
|
109
|
+
}
|
|
110
|
+
catch (e) {
|
|
111
|
+
return { ok: false, message: e instanceof Error ? e.message : String(e) };
|
|
112
|
+
}
|
|
102
113
|
}
|
|
103
114
|
};
|
|
104
115
|
}
|
|
@@ -39,7 +39,14 @@ export interface CheckoutInput {
|
|
|
39
39
|
customerEmail: string;
|
|
40
40
|
customerName?: string;
|
|
41
41
|
customerPhone?: string;
|
|
42
|
+
/** Optional tax id (NIP). Validated server-side; invalid → checkout 400. */
|
|
43
|
+
customerNip?: string;
|
|
44
|
+
customerCompanyName?: string;
|
|
42
45
|
shippingAddress?: Record<string, string>;
|
|
46
|
+
/** Separate billing address for the invoice; falls back to shipping. */
|
|
47
|
+
billingAddress?: Record<string, string>;
|
|
48
|
+
/** B2C opt-in: request an invoice even without a NIP. */
|
|
49
|
+
invoiceRequested?: boolean;
|
|
43
50
|
shippingMethodId: string;
|
|
44
51
|
carrierRef?: string;
|
|
45
52
|
paymentMethod: string;
|
|
@@ -8,6 +8,7 @@ import { insertPaymentRow } from '../server/payments.js';
|
|
|
8
8
|
import { getShippingMethod } from '../server/shipping.js';
|
|
9
9
|
import { checkRateLimit, clientKey } from '../rate-limit.js';
|
|
10
10
|
import { requireShopConfig } from '../server/db.js';
|
|
11
|
+
import { isValidNip } from '../nip.js';
|
|
11
12
|
function shopEnabled() {
|
|
12
13
|
try {
|
|
13
14
|
return getCMS().shopConfig !== null;
|
|
@@ -66,6 +67,12 @@ export function createCheckoutHandler() {
|
|
|
66
67
|
return json({ error: 'shippingMethodId required' }, { status: 400 });
|
|
67
68
|
if (!paymentMethod)
|
|
68
69
|
return json({ error: 'paymentMethod required' }, { status: 400 });
|
|
70
|
+
// Validate the NIP up front — Fakturownia rejects an invalid one and the
|
|
71
|
+
// invoice later goes to KSeF, so we never persist a malformed tax id.
|
|
72
|
+
const customerNip = asString(body.customerNip, 20);
|
|
73
|
+
if (customerNip && !isValidNip(customerNip)) {
|
|
74
|
+
return json({ error: 'Podany NIP jest nieprawidłowy.' }, { status: 400 });
|
|
75
|
+
}
|
|
69
76
|
const cartItems = readCartCookie(cookies);
|
|
70
77
|
if (cartItems.length === 0) {
|
|
71
78
|
return json({ error: 'Cart is empty' }, { status: 400 });
|
|
@@ -98,7 +105,11 @@ export function createCheckoutHandler() {
|
|
|
98
105
|
customerEmail,
|
|
99
106
|
customerName: asString(body.customerName, 200),
|
|
100
107
|
customerPhone: asString(body.customerPhone, 40),
|
|
108
|
+
customerNip,
|
|
109
|
+
customerCompanyName: asString(body.customerCompanyName, 200),
|
|
101
110
|
shippingAddress: asStringRecord(body.shippingAddress),
|
|
111
|
+
billingAddress: asStringRecord(body.billingAddress),
|
|
112
|
+
invoiceRequested: body.invoiceRequested === true,
|
|
102
113
|
shippingMethodId,
|
|
103
114
|
carrierRef,
|
|
104
115
|
paymentMethod,
|
package/dist/shop/index.d.ts
CHANGED
|
@@ -9,5 +9,8 @@ export { stripeAdapter } from './adapters/stripe/index.js';
|
|
|
9
9
|
export type { StripeAdapterOptions } from './adapters/stripe/index.js';
|
|
10
10
|
export { inpostAdapter } from './adapters/inpost/index.js';
|
|
11
11
|
export type { InpostAdapterOptions, InpostSenderAddress, GeowidgetConfigPreset, InpostEnvironment } from './adapters/inpost/index.js';
|
|
12
|
-
export
|
|
12
|
+
export { fakturowniaAdapter } from './adapters/fakturownia/index.js';
|
|
13
|
+
export type { FakturowniaAdapterOptions } from './adapters/fakturownia/index.js';
|
|
14
|
+
export { isValidNip } from './nip.js';
|
|
15
|
+
export type { ShopConfig, ResolvedShopConfig, Currency, Order, OrderStatus, PaymentAdapter, PaymentCreateContext, PaymentRefundInput, PaymentRefundResult, CarrierAdapter, CarrierEvent, ShipmentCreateInput, ShipmentCreateResult, ShipmentLabel, ConsentConfig, ShopFeatures, PaymentCreateResult, PaymentEvent, OrderRef, CouponRef, I18nText, VariantAttribute, VariantAttributeText, VariantAttributeNumber, VariantAttributeDatetime, VariantAttributeSelect, VariantAttributeBoolean, VariantAttributeImage, VariantAttributeEntry, VariantAttributeSlug, VariantLabelConfig, VariantExpiryConfig, PaymentPolicy, DepositAmount, PartialPayment, InvoicingAdapter, InvoiceIssuePolicy, InvoiceBuyer, InvoiceLineItem, InvoicePayload, InvoiceCreateResult, InvoiceContext } from './types.js';
|
|
13
16
|
export { interpolateTemplate } from './template.js';
|
package/dist/shop/index.js
CHANGED
|
@@ -12,6 +12,7 @@ export function defineShop(config) {
|
|
|
12
12
|
webhook: config.rateLimit?.webhook ?? { limit: 60, windowSec: 60 }
|
|
13
13
|
},
|
|
14
14
|
carriers: config.carriers ?? [],
|
|
15
|
+
invoicing: config.invoicing ?? null,
|
|
15
16
|
consents: config.consents ?? [],
|
|
16
17
|
orderViewUrl: config.orderViewUrl ?? '/shop/order/{orderNumber}?token={accessToken}',
|
|
17
18
|
variantAttributes: config.variantAttributes ?? {},
|
|
@@ -25,4 +26,6 @@ export { manualAdapter } from './adapters/manual/index.js';
|
|
|
25
26
|
export { payuAdapter } from './adapters/payu/index.js';
|
|
26
27
|
export { stripeAdapter } from './adapters/stripe/index.js';
|
|
27
28
|
export { inpostAdapter } from './adapters/inpost/index.js';
|
|
29
|
+
export { fakturowniaAdapter } from './adapters/fakturownia/index.js';
|
|
30
|
+
export { isValidNip } from './nip.js';
|
|
28
31
|
export { interpolateTemplate } from './template.js';
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validate a Polish NIP (tax id) — 10 digits with a weighted checksum.
|
|
3
|
+
*
|
|
4
|
+
* Accepts dashes, spaces and an optional `PL` prefix; these are stripped before
|
|
5
|
+
* validation. Returns `false` for any malformed input rather than throwing.
|
|
6
|
+
*
|
|
7
|
+
* Validation is intentionally strict: an invoice issued with an invalid NIP is
|
|
8
|
+
* rejected by Fakturownia and would later fail KSeF submission, so the buyer's
|
|
9
|
+
* NIP is verified at checkout time before it ever reaches the adapter.
|
|
10
|
+
* @public
|
|
11
|
+
*/
|
|
12
|
+
export declare function isValidNip(nip: string): boolean;
|
package/dist/shop/nip.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const NIP_WEIGHTS = [6, 5, 7, 2, 3, 4, 5, 6, 7];
|
|
2
|
+
/**
|
|
3
|
+
* Validate a Polish NIP (tax id) — 10 digits with a weighted checksum.
|
|
4
|
+
*
|
|
5
|
+
* Accepts dashes, spaces and an optional `PL` prefix; these are stripped before
|
|
6
|
+
* validation. Returns `false` for any malformed input rather than throwing.
|
|
7
|
+
*
|
|
8
|
+
* Validation is intentionally strict: an invoice issued with an invalid NIP is
|
|
9
|
+
* rejected by Fakturownia and would later fail KSeF submission, so the buyer's
|
|
10
|
+
* NIP is verified at checkout time before it ever reaches the adapter.
|
|
11
|
+
* @public
|
|
12
|
+
*/
|
|
13
|
+
export function isValidNip(nip) {
|
|
14
|
+
const normalized = nip.replace(/^PL/i, '').replace(/[\s-]/g, '');
|
|
15
|
+
if (!/^\d{10}$/.test(normalized))
|
|
16
|
+
return false;
|
|
17
|
+
const digits = normalized.split('').map(Number);
|
|
18
|
+
const sum = NIP_WEIGHTS.reduce((acc, weight, i) => acc + weight * digits[i], 0);
|
|
19
|
+
const checksum = sum % 11;
|
|
20
|
+
if (checksum === 10)
|
|
21
|
+
return false;
|
|
22
|
+
return checksum === digits[9];
|
|
23
|
+
}
|
|
@@ -51,3 +51,13 @@ export declare function recordCouponRedemption(input: {
|
|
|
51
51
|
* row commits.
|
|
52
52
|
*/
|
|
53
53
|
export declare function releaseCouponSlot(couponId: string): Promise<void>;
|
|
54
|
+
/**
|
|
55
|
+
* Read the coupon applied to an order (if any), via the redemption row.
|
|
56
|
+
* Returns `null` when the order had no coupon or the redemption row is missing.
|
|
57
|
+
* Safe to call from email/admin/storefront — uses the same authoritative
|
|
58
|
+
* `shop_coupon_redemptions` table that `recordCouponRedemption` writes to.
|
|
59
|
+
*/
|
|
60
|
+
export declare function getOrderCoupon(orderId: string): Promise<{
|
|
61
|
+
code: string;
|
|
62
|
+
discountAmount: number;
|
|
63
|
+
} | null>;
|
|
@@ -115,3 +115,22 @@ export async function releaseCouponSlot(couponId) {
|
|
|
115
115
|
})
|
|
116
116
|
.where(eq(shopCouponsTable.id, couponId));
|
|
117
117
|
}
|
|
118
|
+
/**
|
|
119
|
+
* Read the coupon applied to an order (if any), via the redemption row.
|
|
120
|
+
* Returns `null` when the order had no coupon or the redemption row is missing.
|
|
121
|
+
* Safe to call from email/admin/storefront — uses the same authoritative
|
|
122
|
+
* `shop_coupon_redemptions` table that `recordCouponRedemption` writes to.
|
|
123
|
+
*/
|
|
124
|
+
export async function getOrderCoupon(orderId) {
|
|
125
|
+
const db = getShopDb();
|
|
126
|
+
const [row] = await db
|
|
127
|
+
.select({
|
|
128
|
+
code: shopCouponsTable.code,
|
|
129
|
+
discountAmount: shopCouponRedemptionsTable.discountAmount
|
|
130
|
+
})
|
|
131
|
+
.from(shopCouponRedemptionsTable)
|
|
132
|
+
.innerJoin(shopCouponsTable, eq(shopCouponRedemptionsTable.couponId, shopCouponsTable.id))
|
|
133
|
+
.where(eq(shopCouponRedemptionsTable.orderId, orderId))
|
|
134
|
+
.limit(1);
|
|
135
|
+
return row ?? null;
|
|
136
|
+
}
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import type { OrderStatus } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* List of template names a shop install must ship in `dist/shop/templates/`.
|
|
4
|
+
* Consumed by `validateBuiltinTemplates` at CMS bootstrap.
|
|
5
|
+
* @internal
|
|
6
|
+
*/
|
|
7
|
+
export declare const REQUIRED_TEMPLATE_NAMES: readonly string[];
|
|
2
8
|
export declare function sendOrderStatusEmail(orderId: string, status: OrderStatus): Promise<void>;
|
|
3
9
|
/**
|
|
4
10
|
* Notify the shop admin that a product crossed its low-stock threshold.
|
|
5
11
|
* Fire-and-forget — silently no-ops when no `shop.adminEmail` or no email
|
|
6
|
-
* adapter is configured.
|
|
7
|
-
* site centralised (HTML, escaping, logging behaviour identical to status
|
|
8
|
-
* emails).
|
|
12
|
+
* adapter is configured.
|
|
9
13
|
*
|
|
10
14
|
* @internal
|
|
11
15
|
*/
|