includio-cms 0.27.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 +19 -3
- package/CHANGELOG.md +40 -0
- package/DOCS.md +1 -1
- package/dist/admin/client/shop/shop-order-detail-page.svelte +85 -0
- package/dist/admin/remote/shop.remote.d.ts +58 -0
- package/dist/admin/remote/shop.remote.js +18 -0
- 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 +70 -0
- package/dist/db-postgres/schema/shop/order.js +4 -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/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/invoices.d.ts +64 -0
- package/dist/shop/server/invoices.js +237 -0
- package/dist/shop/server/orders.d.ts +4 -0
- package/dist/shop/server/orders.js +11 -0
- package/dist/shop/types.d.ts +67 -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/index.js +3 -1
- package/package.json +1 -1
|
@@ -124,6 +124,40 @@ export declare const shopOrdersTable: import("drizzle-orm/pg-core/table", { with
|
|
|
124
124
|
identity: undefined;
|
|
125
125
|
generated: undefined;
|
|
126
126
|
}, {}, {}>;
|
|
127
|
+
customerNip: import("drizzle-orm/pg-core", { with: { "resolution-mode": "require" } }).PgColumn<{
|
|
128
|
+
name: "customer_nip";
|
|
129
|
+
tableName: "shop_orders";
|
|
130
|
+
dataType: "string";
|
|
131
|
+
columnType: "PgText";
|
|
132
|
+
data: string;
|
|
133
|
+
driverParam: string;
|
|
134
|
+
notNull: false;
|
|
135
|
+
hasDefault: false;
|
|
136
|
+
isPrimaryKey: false;
|
|
137
|
+
isAutoincrement: false;
|
|
138
|
+
hasRuntimeDefault: false;
|
|
139
|
+
enumValues: [string, ...string[]];
|
|
140
|
+
baseColumn: never;
|
|
141
|
+
identity: undefined;
|
|
142
|
+
generated: undefined;
|
|
143
|
+
}, {}, {}>;
|
|
144
|
+
customerCompanyName: import("drizzle-orm/pg-core", { with: { "resolution-mode": "require" } }).PgColumn<{
|
|
145
|
+
name: "customer_company_name";
|
|
146
|
+
tableName: "shop_orders";
|
|
147
|
+
dataType: "string";
|
|
148
|
+
columnType: "PgText";
|
|
149
|
+
data: string;
|
|
150
|
+
driverParam: string;
|
|
151
|
+
notNull: false;
|
|
152
|
+
hasDefault: false;
|
|
153
|
+
isPrimaryKey: false;
|
|
154
|
+
isAutoincrement: false;
|
|
155
|
+
hasRuntimeDefault: false;
|
|
156
|
+
enumValues: [string, ...string[]];
|
|
157
|
+
baseColumn: never;
|
|
158
|
+
identity: undefined;
|
|
159
|
+
generated: undefined;
|
|
160
|
+
}, {}, {}>;
|
|
127
161
|
shippingAddress: import("drizzle-orm/pg-core", { with: { "resolution-mode": "require" } }).PgColumn<{
|
|
128
162
|
name: "shipping_address";
|
|
129
163
|
tableName: "shop_orders";
|
|
@@ -143,6 +177,42 @@ export declare const shopOrdersTable: import("drizzle-orm/pg-core/table", { with
|
|
|
143
177
|
}, {}, {
|
|
144
178
|
$type: Record<string, string>;
|
|
145
179
|
}>;
|
|
180
|
+
billingAddress: import("drizzle-orm/pg-core", { with: { "resolution-mode": "require" } }).PgColumn<{
|
|
181
|
+
name: "billing_address";
|
|
182
|
+
tableName: "shop_orders";
|
|
183
|
+
dataType: "json";
|
|
184
|
+
columnType: "PgJsonb";
|
|
185
|
+
data: Record<string, string>;
|
|
186
|
+
driverParam: unknown;
|
|
187
|
+
notNull: false;
|
|
188
|
+
hasDefault: false;
|
|
189
|
+
isPrimaryKey: false;
|
|
190
|
+
isAutoincrement: false;
|
|
191
|
+
hasRuntimeDefault: false;
|
|
192
|
+
enumValues: undefined;
|
|
193
|
+
baseColumn: never;
|
|
194
|
+
identity: undefined;
|
|
195
|
+
generated: undefined;
|
|
196
|
+
}, {}, {
|
|
197
|
+
$type: Record<string, string>;
|
|
198
|
+
}>;
|
|
199
|
+
invoiceRequested: import("drizzle-orm/pg-core", { with: { "resolution-mode": "require" } }).PgColumn<{
|
|
200
|
+
name: "invoice_requested";
|
|
201
|
+
tableName: "shop_orders";
|
|
202
|
+
dataType: "boolean";
|
|
203
|
+
columnType: "PgBoolean";
|
|
204
|
+
data: boolean;
|
|
205
|
+
driverParam: boolean;
|
|
206
|
+
notNull: true;
|
|
207
|
+
hasDefault: true;
|
|
208
|
+
isPrimaryKey: false;
|
|
209
|
+
isAutoincrement: false;
|
|
210
|
+
hasRuntimeDefault: false;
|
|
211
|
+
enumValues: undefined;
|
|
212
|
+
baseColumn: never;
|
|
213
|
+
identity: undefined;
|
|
214
|
+
generated: undefined;
|
|
215
|
+
}, {}, {}>;
|
|
146
216
|
totalNet: import("drizzle-orm/pg-core", { with: { "resolution-mode": "require" } }).PgColumn<{
|
|
147
217
|
name: "total_net";
|
|
148
218
|
tableName: "shop_orders";
|
|
@@ -8,7 +8,11 @@ export const shopOrdersTable = pgTable('shop_orders', {
|
|
|
8
8
|
customerEmail: text('customer_email').notNull(),
|
|
9
9
|
customerName: text('customer_name'),
|
|
10
10
|
customerPhone: text('customer_phone'),
|
|
11
|
+
customerNip: text('customer_nip'),
|
|
12
|
+
customerCompanyName: text('customer_company_name'),
|
|
11
13
|
shippingAddress: jsonb('shipping_address').$type(),
|
|
14
|
+
billingAddress: jsonb('billing_address').$type(),
|
|
15
|
+
invoiceRequested: boolean('invoice_requested').default(false).notNull(),
|
|
12
16
|
totalNet: integer('total_net').notNull(),
|
|
13
17
|
totalGross: integer('total_gross').notNull(),
|
|
14
18
|
vatAmount: integer('vat_amount').notNull(),
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { FakturowniaInvoiceBody } from './payload.js';
|
|
2
|
+
export interface FakturowniaClientOptions {
|
|
3
|
+
/** Account subdomain (`acme`) or full host/URL (`acme.fakturownia.pl`) — normalised either way. */
|
|
4
|
+
domain: string;
|
|
5
|
+
apiToken: string;
|
|
6
|
+
fetch?: typeof fetch;
|
|
7
|
+
}
|
|
8
|
+
export interface FakturowniaInvoice {
|
|
9
|
+
id: number | string;
|
|
10
|
+
number?: string;
|
|
11
|
+
view_url?: string;
|
|
12
|
+
[key: string]: unknown;
|
|
13
|
+
}
|
|
14
|
+
export declare class FakturowniaApiError extends Error {
|
|
15
|
+
readonly status: number;
|
|
16
|
+
readonly body?: unknown | undefined;
|
|
17
|
+
constructor(message: string, status: number, body?: unknown | undefined);
|
|
18
|
+
}
|
|
19
|
+
export declare class FakturowniaClient {
|
|
20
|
+
private readonly base;
|
|
21
|
+
private readonly apiToken;
|
|
22
|
+
private readonly fetchFn;
|
|
23
|
+
constructor(opts: FakturowniaClientOptions);
|
|
24
|
+
createInvoice(invoice: FakturowniaInvoiceBody): Promise<FakturowniaInvoice>;
|
|
25
|
+
sendByEmail(id: number | string): Promise<void>;
|
|
26
|
+
private postRaw;
|
|
27
|
+
private post;
|
|
28
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
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
|
+
async postRaw(path, payload) {
|
|
44
|
+
const res = await this.fetchFn(`${this.base}${path}`, {
|
|
45
|
+
method: 'POST',
|
|
46
|
+
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
|
47
|
+
body: JSON.stringify(payload)
|
|
48
|
+
});
|
|
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 ${path} → ${res.status}${detail}`, res.status, body);
|
|
60
|
+
}
|
|
61
|
+
return res;
|
|
62
|
+
}
|
|
63
|
+
async post(path, payload) {
|
|
64
|
+
const res = await this.postRaw(path, payload);
|
|
65
|
+
return (await res.json());
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -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,36 @@
|
|
|
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
|
+
return adapter;
|
|
36
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -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, 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
|
+
}
|
|
@@ -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 {};
|