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
package/API.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
# includio-cms — Public API v0.
|
|
1
|
+
# includio-cms — Public API v0.28.0
|
|
2
2
|
|
|
3
3
|
> Auto-generated by `scripts/generate-api-md.ts`. Do not edit by hand.
|
|
4
4
|
|
|
5
|
-
Entry points: **18** · Stable: **
|
|
5
|
+
Entry points: **18** · Stable: **464** · Experimental: **4**
|
|
6
6
|
|
|
7
7
|
Tags:
|
|
8
8
|
- `@public` — frozen for v1.0; semver-protected.
|
|
@@ -223,6 +223,7 @@ Tags:
|
|
|
223
223
|
- `const getMediaTags: <inferred>`
|
|
224
224
|
- `const getMediaTagsWithCounts: <inferred>`
|
|
225
225
|
- `const getOrderForAdmin: <inferred>`
|
|
226
|
+
- `const getOrderInvoiceAdmin: <inferred>`
|
|
226
227
|
- `const getOrderRefundsAdmin: <inferred>`
|
|
227
228
|
- `const getRawEntries: <inferred>`
|
|
228
229
|
- `const getRawEntry: <inferred>`
|
|
@@ -236,6 +237,7 @@ Tags:
|
|
|
236
237
|
- `const getSingles: <inferred>`
|
|
237
238
|
- `const getSubmissionsOverview: <inferred>`
|
|
238
239
|
- `const isAIAvailable: <inferred>`
|
|
240
|
+
- `const issueInvoiceCmd: <inferred>`
|
|
239
241
|
- `const listCouponsAdmin: <inferred>`
|
|
240
242
|
- `const listOrdersAdmin: <inferred>`
|
|
241
243
|
- `const listShippingMethodsAdmin: <inferred>`
|
|
@@ -337,6 +339,8 @@ Tags:
|
|
|
337
339
|
- `const shopCouponRedemptionsTable: <inferred>`
|
|
338
340
|
- `const shopCouponsTable: <inferred>`
|
|
339
341
|
- `type ShopCouponType = 'percent' | 'fixed'`
|
|
342
|
+
- `const shopInvoicesTable: <inferred>`
|
|
343
|
+
- `type ShopInvoiceStatus = 'pending' | 'issued' | 'sent' | 'failed'`
|
|
340
344
|
- `const shopOrderItemsTable: <inferred>`
|
|
341
345
|
- `const shopOrdersTable: <inferred>`
|
|
342
346
|
- `const shopOrderStatusHistoryTable: <inferred>`
|
|
@@ -377,6 +381,8 @@ Tags:
|
|
|
377
381
|
- `const shopCouponRedemptionsTable: <inferred>`
|
|
378
382
|
- `const shopCouponsTable: <inferred>`
|
|
379
383
|
- `type ShopCouponType = 'percent' | 'fixed'`
|
|
384
|
+
- `const shopInvoicesTable: <inferred>`
|
|
385
|
+
- `type ShopInvoiceStatus = 'pending' | 'issued' | 'sent' | 'failed'`
|
|
380
386
|
- `const shopOrderItemsTable: <inferred>`
|
|
381
387
|
- `const shopOrdersTable: <inferred>`
|
|
382
388
|
- `const shopOrderStatusHistoryTable: <inferred>`
|
|
@@ -411,7 +417,9 @@ Tags:
|
|
|
411
417
|
- `interface CouponRef`
|
|
412
418
|
- `type Currency = 'PLN'`
|
|
413
419
|
- `defineShop(config: ShopConfig): ResolvedShopConfig`
|
|
414
|
-
- `type DepositAmount =
|
|
420
|
+
- `type DepositAmount = { type: 'percent'; value: number } | { type: 'amount'; value: number }` — Deposit amount specifier. `percent` charges `floor(base * value / 100)` of
|
|
421
|
+
- `fakturowniaAdapter(opts: FakturowniaAdapterOptions): InvoicingAdapter` — Invoicing adapter backed by Fakturownia (fakturownia.pl). Issues a paid VAT
|
|
422
|
+
- `interface FakturowniaAdapterOptions`
|
|
415
423
|
- `filterUpcoming(variants: T[], config: VariantExpiryConfig | null, now?: Date): T[]` — Return the subset of `variants` that have not yet expired. Order is
|
|
416
424
|
- `type GeowidgetConfigPreset = 'parcelcollect' | 'parcelsend' | 'parcelcollect247' | string`
|
|
417
425
|
- `type I18nText = { [lang: string]: string; }`
|
|
@@ -421,6 +429,14 @@ Tags:
|
|
|
421
429
|
- `interface InpostSenderAddress`
|
|
422
430
|
- `interpolateTemplate(template: string, vars: Record<string, unknown>, locale: string): string` — String interpolation engine used by `defineShop({ variantLabel.template })`
|
|
423
431
|
- `class InvalidVariantAttributesError`
|
|
432
|
+
- `interface InvoiceBuyer`
|
|
433
|
+
- `interface InvoiceContext`
|
|
434
|
+
- `interface InvoiceCreateResult`
|
|
435
|
+
- `type InvoiceIssuePolicy = 'b2bAndOnRequest' | 'always'` — When an invoice should be issued automatically after an order is fully paid.
|
|
436
|
+
- `interface InvoiceLineItem`
|
|
437
|
+
- `interface InvoicePayload`
|
|
438
|
+
- `interface InvoicingAdapter` — Adapter that issues invoices with an external provider (e.g. Fakturownia).
|
|
439
|
+
- `isValidNip(nip: string): boolean` — Validate a Polish NIP (tax id) — 10 digits with a weighted checksum.
|
|
424
440
|
- `isVariantExpired(variant: VariantLike, config: VariantExpiryConfig | null, now: Date = new Date(Date.now())): boolean` — Check whether a variant has expired under the given config. Fail-open by
|
|
425
441
|
- `manualAdapter(opts: ManualPaymentAdapterOptions = {}): PaymentAdapter` — Manual payment adapter — order goes to awaitingPayment, admin marks as paid later
|
|
426
442
|
- `interface OrderRef`
|
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,46 @@
|
|
|
3
3
|
All notable changes to includio-cms are documented here.
|
|
4
4
|
Generated from `src/lib/updates/` — do not edit manually.
|
|
5
5
|
|
|
6
|
+
## 0.28.0 — 2026-06-01
|
|
7
|
+
|
|
8
|
+
Fakturowanie — integracja sklepu z Fakturownią: automatyczne wystawianie i wysyłka faktury po opłaceniu zamówienia. Nowy generyczny `InvoicingAdapter` (spójny z payment/carrier) + `fakturowniaAdapter()` jako pierwsza implementacja. Faktura wystawiana fire-and-forget gdy zamówienie jest w pełni opłacone (`!balanceOwed` — zaliczki czekają na dopłatę balansu), wysyłana przez Fakturownię (`send_by_email`). Trigger konfigurowalny per-adapter (`issueWhen`: `b2bAndOnRequest` domyślnie / `always`). Checkout zbiera dane B2B (NIP z twardą walidacją sumy kontrolnej, nazwa firmy, adres do faktury, opt-in „chcę fakturę"). Idempotencja przez `shop_invoices` (unikat per zamówienie); ręczny retry w adminie. Additive only — żadnych breaking zmian.
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- `defineShop({ invoicing })` (`includio-cms/shop`) — nowy opcjonalny slot adaptera fakturowania, analogiczny do `payment` / `carriers`. `InvoicingAdapter { id, issueWhen?, createInvoice(payload, ctx), send?(externalId, ctx) }` + typy payloadu (`InvoicePayload`, `InvoiceBuyer`, `InvoiceLineItem`, `InvoiceCreateResult`, `InvoiceContext`, `InvoiceIssuePolicy`) wyeksportowane jako `@public` z `includio-cms/shop`.
|
|
12
|
+
- `fakturowniaAdapter({ domain, apiToken, kind?, issueWhen?, sendEmail? })` (`includio-cms/shop`, `@public`) — adapter dla Fakturowni (fakturownia.pl). `createInvoice` mapuje zamówienie na fakturę VAT oznaczoną jako opłacona (`paid_date` = data potwierdzenia płatności), `send` woła `send_by_email` (domyślnie włączone). Numeracja i dane sprzedawcy zarządzane po stronie konta Fakturowni. Sekret `apiToken` przekazywany przez konsumenta z `$env/dynamic/private` (jak `STRIPE_SECRET_KEY`).
|
|
13
|
+
- Auto-faktura po opłaceniu — `maybeIssueInvoiceForOrder(orderId)` (`$lib/shop/server/invoices.ts`) wołane fire-and-forget z `updateOrderStatus` (przy `paid`) oraz z `markBalancePaid` (po dopłacie balansu). Fail-open: błąd providera nie blokuje webhooka płatności. Guard wystawia fakturę tylko gdy zamówienie w pełni opłacone (`status ∈ {paid, preparing, sent, done}` && `!balanceOwed`) — deposit czeka na dopłatę.
|
|
14
|
+
- Konfigurowalny trigger `issueWhen`: `b2bAndOnRequest` (domyślny — faktura gdy podano NIP lub zaznaczono „chcę fakturę") albo `always` (każde opłacone zamówienie, np. dla klienta wymagającego faktur również dla osób fizycznych).
|
|
15
|
+
- `shop_invoices` (nowa tabela) — jedna faktura per zamówienie (`order_id` UNIQUE → idempotencja). Pola `status` (`pending`/`issued`/`sent`/`failed`), `external_id`, `number`, `pdf_url`, `attempts`, `next_retry_at`, `last_error`, `raw`. Kolumny `attempts`/`next_retry_at` zarezerwowane pod przyszły automatyczny retry (obecnie tylko ręczny).
|
|
16
|
+
- Checkout B2B — `shop_orders` dostaje kolumny `customer_nip`, `customer_company_name`, `billing_address` (jsonb), `invoice_requested` (boolean). `createOrderFromCart` / `checkout-handler` przyjmują i zapisują te pola; `CheckoutInput` (`includio-cms/shop/client`) rozszerzony. Storefront form (pola NIP/firma/checkbox/adres) = warstwa konsumenta.
|
|
17
|
+
- `isValidNip(nip)` (`includio-cms/shop`, `@public`) — twarda walidacja polskiego NIP (10 cyfr + suma kontrolna, wagi `[6,5,7,2,3,4,5,6,7]`). Checkout odrzuca nieprawidłowy NIP (400) zanim trafi do bazy — Fakturownia odrzuciłaby błędny NIP, a faktura idzie dalej do KSeF, więc walidujemy u źródła.
|
|
18
|
+
- `getOrderInvoiceAdmin` (query) + `issueInvoiceCmd` (command) — admin remote functions (`includio-cms/admin/remote`). `shop-order-detail-page.svelte` zyskuje sekcję „Faktura": numer, link do PDF, status, przycisk „Wystaw fakturę" / „Wystaw ponownie" (ręczny retry, `force`). Auth-protected (`requireAuth`).
|
|
19
|
+
|
|
20
|
+
### Migration
|
|
21
|
+
|
|
22
|
+
```sql
|
|
23
|
+
CREATE TABLE IF NOT EXISTS shop_invoices (
|
|
24
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
25
|
+
order_id uuid NOT NULL UNIQUE REFERENCES shop_orders(id) ON DELETE CASCADE,
|
|
26
|
+
provider text NOT NULL,
|
|
27
|
+
external_id text,
|
|
28
|
+
number text,
|
|
29
|
+
pdf_url text,
|
|
30
|
+
kind text NOT NULL DEFAULT 'vat',
|
|
31
|
+
status text NOT NULL DEFAULT 'pending',
|
|
32
|
+
attempts integer NOT NULL DEFAULT 0,
|
|
33
|
+
next_retry_at timestamptz,
|
|
34
|
+
last_error text,
|
|
35
|
+
raw jsonb,
|
|
36
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
37
|
+
updated_at timestamptz NOT NULL DEFAULT now()
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
ALTER TABLE shop_orders ADD COLUMN IF NOT EXISTS customer_nip text;
|
|
41
|
+
ALTER TABLE shop_orders ADD COLUMN IF NOT EXISTS customer_company_name text;
|
|
42
|
+
ALTER TABLE shop_orders ADD COLUMN IF NOT EXISTS billing_address jsonb;
|
|
43
|
+
ALTER TABLE shop_orders ADD COLUMN IF NOT EXISTS invoice_requested boolean NOT NULL DEFAULT false;
|
|
44
|
+
```
|
|
45
|
+
|
|
6
46
|
## 0.27.0 — 2026-05-29
|
|
7
47
|
|
|
8
48
|
Shop platformowy — Fazy 1 + 2 + 3 + 4 + 5: schema-driven `variantAttributes` (typed `defineShop` schema, Zod walidacja, GIN index helper, ts-gen typed `variant.attributes`) + admin renderer per attribute type + `variantLabel.template` interpolacja z admin auto-prefill nazwy wariantu + `variantExpiry` opt-in (storefront filter, cart/checkout guard, admin "Zakończony" badge) + `paymentPolicy` A-lite (deposit + balance + signed token + refund per kind + admin balance link + per-kind refund dialog). Faza 6 envet pilot (consumer-side end-to-end QA: szkolenia variants + deposit flow + balance link — nie wymaga zmian w core). Post-Faza-5 hot-fixy uwzględnione w tym wydaniu (single release, nie patch): datetime walidator z offsetem, auto-detect deposit kind przy admin manual mark-paid, admin paymentPolicy UI + preserve-on-undefined w upsert. Additive only — żadnych breaking zmian. Plan: `/Users/patryk/.claude/plans/implementacja-ariacms-0-27-partitioned-kahn.md`.
|
package/DOCS.md
CHANGED
|
@@ -36,6 +36,37 @@
|
|
|
36
36
|
const refundsQuery = $derived(remotes.getOrderRefundsAdmin(orderId));
|
|
37
37
|
let refundDialogOpen = $state(false);
|
|
38
38
|
|
|
39
|
+
const invoiceQuery = $derived(remotes.getOrderInvoiceAdmin(orderId));
|
|
40
|
+
let issuingInvoice = $state(false);
|
|
41
|
+
let invoiceError = $state<string | null>(null);
|
|
42
|
+
|
|
43
|
+
async function issueInvoice(force: boolean) {
|
|
44
|
+
issuingInvoice = true;
|
|
45
|
+
invoiceError = null;
|
|
46
|
+
try {
|
|
47
|
+
const result = await remotes.issueInvoiceCmd({ orderId, force });
|
|
48
|
+
if (!result.success) invoiceError = result.error;
|
|
49
|
+
await invoiceQuery.refresh();
|
|
50
|
+
} finally {
|
|
51
|
+
issuingInvoice = false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function invoiceStatusLabel(status: string): string {
|
|
56
|
+
switch (status) {
|
|
57
|
+
case 'pending':
|
|
58
|
+
return 'W trakcie';
|
|
59
|
+
case 'issued':
|
|
60
|
+
return 'Wystawiona';
|
|
61
|
+
case 'sent':
|
|
62
|
+
return 'Wysłana';
|
|
63
|
+
case 'failed':
|
|
64
|
+
return 'Błąd';
|
|
65
|
+
default:
|
|
66
|
+
return status;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
39
70
|
type OrderStatus =
|
|
40
71
|
| 'new'
|
|
41
72
|
| 'awaitingPayment'
|
|
@@ -501,6 +532,60 @@
|
|
|
501
532
|
</section>
|
|
502
533
|
{/if}
|
|
503
534
|
|
|
535
|
+
{#if invoiceQuery.ready && invoiceQuery.current?.invoicingEnabled}
|
|
536
|
+
{@const inv = invoiceQuery.current.invoice}
|
|
537
|
+
{@const orderPaid =
|
|
538
|
+
order.status === 'paid' ||
|
|
539
|
+
order.status === 'preparing' ||
|
|
540
|
+
order.status === 'sent' ||
|
|
541
|
+
order.status === 'done'}
|
|
542
|
+
<section class="border-border bg-card space-y-3 rounded-xl border p-3 text-sm sm:p-5">
|
|
543
|
+
<h2 class="text-base font-bold">Faktura</h2>
|
|
544
|
+
{#if inv}
|
|
545
|
+
<div class="space-y-1">
|
|
546
|
+
<div class="flex items-center justify-between gap-2">
|
|
547
|
+
<span class="font-medium">{inv.number ?? '—'}</span>
|
|
548
|
+
<span class="text-muted-foreground text-xs">{invoiceStatusLabel(inv.status)}</span>
|
|
549
|
+
</div>
|
|
550
|
+
{#if inv.pdfUrl}
|
|
551
|
+
<a
|
|
552
|
+
href={inv.pdfUrl}
|
|
553
|
+
target="_blank"
|
|
554
|
+
rel="noopener noreferrer"
|
|
555
|
+
class="text-primary text-xs underline"
|
|
556
|
+
>
|
|
557
|
+
Otwórz fakturę
|
|
558
|
+
</a>
|
|
559
|
+
{/if}
|
|
560
|
+
{#if inv.status === 'failed' && inv.lastError}
|
|
561
|
+
<p class="text-destructive text-xs">{inv.lastError}</p>
|
|
562
|
+
{/if}
|
|
563
|
+
</div>
|
|
564
|
+
{:else}
|
|
565
|
+
<p class="text-muted-foreground text-xs">Brak faktury dla tego zamówienia.</p>
|
|
566
|
+
{/if}
|
|
567
|
+
{#if invoiceError}
|
|
568
|
+
<p class="text-destructive text-xs">{invoiceError}</p>
|
|
569
|
+
{/if}
|
|
570
|
+
{#if orderPaid}
|
|
571
|
+
<Button
|
|
572
|
+
onclick={() => issueInvoice(true)}
|
|
573
|
+
variant="outline"
|
|
574
|
+
class="w-full"
|
|
575
|
+
disabled={issuingInvoice}
|
|
576
|
+
>
|
|
577
|
+
{inv && (inv.status === 'issued' || inv.status === 'sent')
|
|
578
|
+
? 'Wystaw ponownie'
|
|
579
|
+
: 'Wystaw fakturę'}
|
|
580
|
+
</Button>
|
|
581
|
+
{:else}
|
|
582
|
+
<p class="text-muted-foreground text-xs">
|
|
583
|
+
Fakturę można wystawić po opłaceniu zamówienia.
|
|
584
|
+
</p>
|
|
585
|
+
{/if}
|
|
586
|
+
</section>
|
|
587
|
+
{/if}
|
|
588
|
+
|
|
504
589
|
{#if order.carrierType && order.carrierType !== 'none'}
|
|
505
590
|
<section class="border-border bg-card space-y-3 rounded-xl border p-3 text-sm sm:p-5">
|
|
506
591
|
<h2 class="text-base font-bold">
|
|
@@ -114,7 +114,11 @@ export declare const listOrdersAdmin: import("@sveltejs/kit").RemoteQueryFunctio
|
|
|
114
114
|
customerEmail: string;
|
|
115
115
|
customerName: string | null;
|
|
116
116
|
customerPhone: string | null;
|
|
117
|
+
customerNip: string | null;
|
|
118
|
+
customerCompanyName: string | null;
|
|
117
119
|
shippingAddress: Record<string, string> | null;
|
|
120
|
+
billingAddress: Record<string, string> | null;
|
|
121
|
+
invoiceRequested: boolean;
|
|
118
122
|
totalNet: number;
|
|
119
123
|
totalGross: number;
|
|
120
124
|
vatAmount: number;
|
|
@@ -155,7 +159,11 @@ export declare const getOrderForAdmin: import("@sveltejs/kit").RemoteQueryFuncti
|
|
|
155
159
|
customerEmail: string;
|
|
156
160
|
customerName: string | null;
|
|
157
161
|
customerPhone: string | null;
|
|
162
|
+
customerNip: string | null;
|
|
163
|
+
customerCompanyName: string | null;
|
|
158
164
|
shippingAddress: Record<string, string> | null;
|
|
165
|
+
billingAddress: Record<string, string> | null;
|
|
166
|
+
invoiceRequested: boolean;
|
|
159
167
|
totalNet: number;
|
|
160
168
|
totalGross: number;
|
|
161
169
|
vatAmount: number;
|
|
@@ -216,7 +224,11 @@ export declare const updateOrderStatusCmd: import("@sveltejs/kit").RemoteCommand
|
|
|
216
224
|
customerEmail: string;
|
|
217
225
|
customerName: string | null;
|
|
218
226
|
customerPhone: string | null;
|
|
227
|
+
customerNip: string | null;
|
|
228
|
+
customerCompanyName: string | null;
|
|
219
229
|
shippingAddress: Record<string, string> | null;
|
|
230
|
+
billingAddress: Record<string, string> | null;
|
|
231
|
+
invoiceRequested: boolean;
|
|
220
232
|
totalNet: number;
|
|
221
233
|
totalGross: number;
|
|
222
234
|
vatAmount: number;
|
|
@@ -312,6 +324,52 @@ export declare const refundOrderCmd: import("@sveltejs/kit").RemoteCommand<{
|
|
|
312
324
|
code: "order_not_found" | "order_not_paid" | "no_provider_ref" | "unknown_provider" | "refund_unsupported" | "invalid_amount" | "amount_exceeds_remaining" | "provider_error" | "no_payment_kind";
|
|
313
325
|
error: string;
|
|
314
326
|
}>>;
|
|
327
|
+
export declare const getOrderInvoiceAdmin: import("@sveltejs/kit").RemoteQueryFunction<string, {
|
|
328
|
+
invoice: {
|
|
329
|
+
number: string | null;
|
|
330
|
+
raw: unknown;
|
|
331
|
+
id: string;
|
|
332
|
+
status: import("../../db-postgres/schema/shop/index.js").ShopInvoiceStatus;
|
|
333
|
+
createdAt: Date;
|
|
334
|
+
updatedAt: Date;
|
|
335
|
+
orderId: string;
|
|
336
|
+
provider: string;
|
|
337
|
+
kind: string;
|
|
338
|
+
externalId: string | null;
|
|
339
|
+
pdfUrl: string | null;
|
|
340
|
+
attempts: number;
|
|
341
|
+
nextRetryAt: Date | null;
|
|
342
|
+
lastError: string | null;
|
|
343
|
+
} | null;
|
|
344
|
+
invoicingEnabled: boolean;
|
|
345
|
+
}>;
|
|
346
|
+
export declare const issueInvoiceCmd: import("@sveltejs/kit").RemoteCommand<{
|
|
347
|
+
orderId: string;
|
|
348
|
+
force?: boolean | undefined;
|
|
349
|
+
}, Promise<{
|
|
350
|
+
success: true;
|
|
351
|
+
invoice: {
|
|
352
|
+
number: string | null;
|
|
353
|
+
raw: unknown;
|
|
354
|
+
id: string;
|
|
355
|
+
status: import("../../db-postgres/schema/shop/index.js").ShopInvoiceStatus;
|
|
356
|
+
createdAt: Date;
|
|
357
|
+
updatedAt: Date;
|
|
358
|
+
orderId: string;
|
|
359
|
+
provider: string;
|
|
360
|
+
kind: string;
|
|
361
|
+
externalId: string | null;
|
|
362
|
+
pdfUrl: string | null;
|
|
363
|
+
attempts: number;
|
|
364
|
+
nextRetryAt: Date | null;
|
|
365
|
+
lastError: string | null;
|
|
366
|
+
} | null;
|
|
367
|
+
error?: undefined;
|
|
368
|
+
} | {
|
|
369
|
+
success: false;
|
|
370
|
+
error: string;
|
|
371
|
+
invoice?: undefined;
|
|
372
|
+
}>>;
|
|
315
373
|
export declare const generateBalanceLinkForOrder: import("@sveltejs/kit").RemoteCommand<string, Promise<{
|
|
316
374
|
success: false;
|
|
317
375
|
error: string;
|
|
@@ -6,6 +6,7 @@ import { createShippingMethod, deleteShippingMethod, getShippingMethod, listShip
|
|
|
6
6
|
import { countOrders, getOrderById, getOrderItems, getOrderStatusHistory, listOrders, updateOrderStatus } from '../../shop/server/orders.js';
|
|
7
7
|
import { cancelShipmentForOrder, createShipmentForOrder } from '../../shop/server/shipments.js';
|
|
8
8
|
import { sendOrderStatusEmail } from '../../shop/server/email.js';
|
|
9
|
+
import { getInvoiceByOrderId, issueInvoiceForOrder } from '../../shop/server/invoices.js';
|
|
9
10
|
import { getRefundedAmount, listRefunds, refundOrder, RefundError } from '../../shop/server/refund.js';
|
|
10
11
|
import { getShopDb } from '../../shop/server/db.js';
|
|
11
12
|
import { shopCouponsTable } from '../../db-postgres/schema/shop/index.js';
|
|
@@ -261,6 +262,23 @@ export const refundOrderCmd = command(z.object({
|
|
|
261
262
|
return { success: false, code: 'provider_error', error: message };
|
|
262
263
|
}
|
|
263
264
|
});
|
|
265
|
+
export const getOrderInvoiceAdmin = query(z.string(), async (orderId) => {
|
|
266
|
+
requireAuth();
|
|
267
|
+
const invoice = await getInvoiceByOrderId(orderId);
|
|
268
|
+
const invoicingEnabled = !!getCMS().shopConfig?.invoicing;
|
|
269
|
+
return { invoice, invoicingEnabled };
|
|
270
|
+
});
|
|
271
|
+
export const issueInvoiceCmd = command(z.object({ orderId: z.string(), force: z.boolean().optional() }), async ({ orderId, force }) => {
|
|
272
|
+
requireAuth();
|
|
273
|
+
try {
|
|
274
|
+
const invoice = await issueInvoiceForOrder(orderId, { force });
|
|
275
|
+
return { success: true, invoice };
|
|
276
|
+
}
|
|
277
|
+
catch (err) {
|
|
278
|
+
const message = err instanceof Error ? err.message : 'Invoice issuance failed';
|
|
279
|
+
return { success: false, error: message };
|
|
280
|
+
}
|
|
281
|
+
});
|
|
264
282
|
export const generateBalanceLinkForOrder = command(z.string(), async (orderId) => {
|
|
265
283
|
requireAuth();
|
|
266
284
|
const { generateBalanceToken, requireBalanceTokenSecret } = await import('../../shop/server/balance-payment.js');
|
|
@@ -7,6 +7,7 @@ export * from './orderStatusHistory.js';
|
|
|
7
7
|
export * from './payment.js';
|
|
8
8
|
export * from './stockReservation.js';
|
|
9
9
|
export * from './refunds.js';
|
|
10
|
+
export * from './invoice.js';
|
|
10
11
|
export * from './webhookEvents.js';
|
|
11
12
|
export * from './coupons.js';
|
|
12
13
|
export * from './couponRedemptions.js';
|
|
@@ -7,6 +7,7 @@ export * from './orderStatusHistory.js';
|
|
|
7
7
|
export * from './payment.js';
|
|
8
8
|
export * from './stockReservation.js';
|
|
9
9
|
export * from './refunds.js';
|
|
10
|
+
export * from './invoice.js';
|
|
10
11
|
export * from './webhookEvents.js';
|
|
11
12
|
export * from './coupons.js';
|
|
12
13
|
export * from './couponRedemptions.js';
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
export type ShopInvoiceStatus = 'pending' | 'issued' | 'sent' | 'failed';
|
|
2
|
+
/**
|
|
3
|
+
* One invoice per order (`order_id` is unique → idempotency guard). `external_id`
|
|
4
|
+
* / `number` / `pdf_url` come from the invoicing provider once issued. The
|
|
5
|
+
* `attempts` / `next_retry_at` columns are reserved for a future automatic-retry
|
|
6
|
+
* mechanism; the current flow only retries manually from the admin.
|
|
7
|
+
*/
|
|
8
|
+
export declare const shopInvoicesTable: import("drizzle-orm/pg-core/table", { with: { "resolution-mode": "require" } }).PgTableWithColumns<{
|
|
9
|
+
name: "shop_invoices";
|
|
10
|
+
schema: undefined;
|
|
11
|
+
columns: {
|
|
12
|
+
id: import("drizzle-orm/pg-core", { with: { "resolution-mode": "require" } }).PgColumn<{
|
|
13
|
+
name: "id";
|
|
14
|
+
tableName: "shop_invoices";
|
|
15
|
+
dataType: "string";
|
|
16
|
+
columnType: "PgUUID";
|
|
17
|
+
data: string;
|
|
18
|
+
driverParam: string;
|
|
19
|
+
notNull: true;
|
|
20
|
+
hasDefault: true;
|
|
21
|
+
isPrimaryKey: true;
|
|
22
|
+
isAutoincrement: false;
|
|
23
|
+
hasRuntimeDefault: false;
|
|
24
|
+
enumValues: undefined;
|
|
25
|
+
baseColumn: never;
|
|
26
|
+
identity: undefined;
|
|
27
|
+
generated: undefined;
|
|
28
|
+
}, {}, {}>;
|
|
29
|
+
orderId: import("drizzle-orm/pg-core", { with: { "resolution-mode": "require" } }).PgColumn<{
|
|
30
|
+
name: "order_id";
|
|
31
|
+
tableName: "shop_invoices";
|
|
32
|
+
dataType: "string";
|
|
33
|
+
columnType: "PgUUID";
|
|
34
|
+
data: string;
|
|
35
|
+
driverParam: string;
|
|
36
|
+
notNull: true;
|
|
37
|
+
hasDefault: false;
|
|
38
|
+
isPrimaryKey: false;
|
|
39
|
+
isAutoincrement: false;
|
|
40
|
+
hasRuntimeDefault: false;
|
|
41
|
+
enumValues: undefined;
|
|
42
|
+
baseColumn: never;
|
|
43
|
+
identity: undefined;
|
|
44
|
+
generated: undefined;
|
|
45
|
+
}, {}, {}>;
|
|
46
|
+
provider: import("drizzle-orm/pg-core", { with: { "resolution-mode": "require" } }).PgColumn<{
|
|
47
|
+
name: "provider";
|
|
48
|
+
tableName: "shop_invoices";
|
|
49
|
+
dataType: "string";
|
|
50
|
+
columnType: "PgText";
|
|
51
|
+
data: string;
|
|
52
|
+
driverParam: string;
|
|
53
|
+
notNull: true;
|
|
54
|
+
hasDefault: false;
|
|
55
|
+
isPrimaryKey: false;
|
|
56
|
+
isAutoincrement: false;
|
|
57
|
+
hasRuntimeDefault: false;
|
|
58
|
+
enumValues: [string, ...string[]];
|
|
59
|
+
baseColumn: never;
|
|
60
|
+
identity: undefined;
|
|
61
|
+
generated: undefined;
|
|
62
|
+
}, {}, {}>;
|
|
63
|
+
externalId: import("drizzle-orm/pg-core", { with: { "resolution-mode": "require" } }).PgColumn<{
|
|
64
|
+
name: "external_id";
|
|
65
|
+
tableName: "shop_invoices";
|
|
66
|
+
dataType: "string";
|
|
67
|
+
columnType: "PgText";
|
|
68
|
+
data: string;
|
|
69
|
+
driverParam: string;
|
|
70
|
+
notNull: false;
|
|
71
|
+
hasDefault: false;
|
|
72
|
+
isPrimaryKey: false;
|
|
73
|
+
isAutoincrement: false;
|
|
74
|
+
hasRuntimeDefault: false;
|
|
75
|
+
enumValues: [string, ...string[]];
|
|
76
|
+
baseColumn: never;
|
|
77
|
+
identity: undefined;
|
|
78
|
+
generated: undefined;
|
|
79
|
+
}, {}, {}>;
|
|
80
|
+
number: import("drizzle-orm/pg-core", { with: { "resolution-mode": "require" } }).PgColumn<{
|
|
81
|
+
name: "number";
|
|
82
|
+
tableName: "shop_invoices";
|
|
83
|
+
dataType: "string";
|
|
84
|
+
columnType: "PgText";
|
|
85
|
+
data: string;
|
|
86
|
+
driverParam: string;
|
|
87
|
+
notNull: false;
|
|
88
|
+
hasDefault: false;
|
|
89
|
+
isPrimaryKey: false;
|
|
90
|
+
isAutoincrement: false;
|
|
91
|
+
hasRuntimeDefault: false;
|
|
92
|
+
enumValues: [string, ...string[]];
|
|
93
|
+
baseColumn: never;
|
|
94
|
+
identity: undefined;
|
|
95
|
+
generated: undefined;
|
|
96
|
+
}, {}, {}>;
|
|
97
|
+
pdfUrl: import("drizzle-orm/pg-core", { with: { "resolution-mode": "require" } }).PgColumn<{
|
|
98
|
+
name: "pdf_url";
|
|
99
|
+
tableName: "shop_invoices";
|
|
100
|
+
dataType: "string";
|
|
101
|
+
columnType: "PgText";
|
|
102
|
+
data: string;
|
|
103
|
+
driverParam: string;
|
|
104
|
+
notNull: false;
|
|
105
|
+
hasDefault: false;
|
|
106
|
+
isPrimaryKey: false;
|
|
107
|
+
isAutoincrement: false;
|
|
108
|
+
hasRuntimeDefault: false;
|
|
109
|
+
enumValues: [string, ...string[]];
|
|
110
|
+
baseColumn: never;
|
|
111
|
+
identity: undefined;
|
|
112
|
+
generated: undefined;
|
|
113
|
+
}, {}, {}>;
|
|
114
|
+
kind: import("drizzle-orm/pg-core", { with: { "resolution-mode": "require" } }).PgColumn<{
|
|
115
|
+
name: "kind";
|
|
116
|
+
tableName: "shop_invoices";
|
|
117
|
+
dataType: "string";
|
|
118
|
+
columnType: "PgText";
|
|
119
|
+
data: string;
|
|
120
|
+
driverParam: string;
|
|
121
|
+
notNull: true;
|
|
122
|
+
hasDefault: true;
|
|
123
|
+
isPrimaryKey: false;
|
|
124
|
+
isAutoincrement: false;
|
|
125
|
+
hasRuntimeDefault: false;
|
|
126
|
+
enumValues: [string, ...string[]];
|
|
127
|
+
baseColumn: never;
|
|
128
|
+
identity: undefined;
|
|
129
|
+
generated: undefined;
|
|
130
|
+
}, {}, {}>;
|
|
131
|
+
status: import("drizzle-orm/pg-core", { with: { "resolution-mode": "require" } }).PgColumn<{
|
|
132
|
+
name: "status";
|
|
133
|
+
tableName: "shop_invoices";
|
|
134
|
+
dataType: "string";
|
|
135
|
+
columnType: "PgText";
|
|
136
|
+
data: ShopInvoiceStatus;
|
|
137
|
+
driverParam: string;
|
|
138
|
+
notNull: true;
|
|
139
|
+
hasDefault: true;
|
|
140
|
+
isPrimaryKey: false;
|
|
141
|
+
isAutoincrement: false;
|
|
142
|
+
hasRuntimeDefault: false;
|
|
143
|
+
enumValues: [string, ...string[]];
|
|
144
|
+
baseColumn: never;
|
|
145
|
+
identity: undefined;
|
|
146
|
+
generated: undefined;
|
|
147
|
+
}, {}, {
|
|
148
|
+
$type: ShopInvoiceStatus;
|
|
149
|
+
}>;
|
|
150
|
+
attempts: import("drizzle-orm/pg-core", { with: { "resolution-mode": "require" } }).PgColumn<{
|
|
151
|
+
name: "attempts";
|
|
152
|
+
tableName: "shop_invoices";
|
|
153
|
+
dataType: "number";
|
|
154
|
+
columnType: "PgInteger";
|
|
155
|
+
data: number;
|
|
156
|
+
driverParam: string | number;
|
|
157
|
+
notNull: true;
|
|
158
|
+
hasDefault: true;
|
|
159
|
+
isPrimaryKey: false;
|
|
160
|
+
isAutoincrement: false;
|
|
161
|
+
hasRuntimeDefault: false;
|
|
162
|
+
enumValues: undefined;
|
|
163
|
+
baseColumn: never;
|
|
164
|
+
identity: undefined;
|
|
165
|
+
generated: undefined;
|
|
166
|
+
}, {}, {}>;
|
|
167
|
+
nextRetryAt: import("drizzle-orm/pg-core", { with: { "resolution-mode": "require" } }).PgColumn<{
|
|
168
|
+
name: "next_retry_at";
|
|
169
|
+
tableName: "shop_invoices";
|
|
170
|
+
dataType: "date";
|
|
171
|
+
columnType: "PgTimestamp";
|
|
172
|
+
data: Date;
|
|
173
|
+
driverParam: string;
|
|
174
|
+
notNull: false;
|
|
175
|
+
hasDefault: false;
|
|
176
|
+
isPrimaryKey: false;
|
|
177
|
+
isAutoincrement: false;
|
|
178
|
+
hasRuntimeDefault: false;
|
|
179
|
+
enumValues: undefined;
|
|
180
|
+
baseColumn: never;
|
|
181
|
+
identity: undefined;
|
|
182
|
+
generated: undefined;
|
|
183
|
+
}, {}, {}>;
|
|
184
|
+
lastError: import("drizzle-orm/pg-core", { with: { "resolution-mode": "require" } }).PgColumn<{
|
|
185
|
+
name: "last_error";
|
|
186
|
+
tableName: "shop_invoices";
|
|
187
|
+
dataType: "string";
|
|
188
|
+
columnType: "PgText";
|
|
189
|
+
data: string;
|
|
190
|
+
driverParam: string;
|
|
191
|
+
notNull: false;
|
|
192
|
+
hasDefault: false;
|
|
193
|
+
isPrimaryKey: false;
|
|
194
|
+
isAutoincrement: false;
|
|
195
|
+
hasRuntimeDefault: false;
|
|
196
|
+
enumValues: [string, ...string[]];
|
|
197
|
+
baseColumn: never;
|
|
198
|
+
identity: undefined;
|
|
199
|
+
generated: undefined;
|
|
200
|
+
}, {}, {}>;
|
|
201
|
+
raw: import("drizzle-orm/pg-core", { with: { "resolution-mode": "require" } }).PgColumn<{
|
|
202
|
+
name: "raw";
|
|
203
|
+
tableName: "shop_invoices";
|
|
204
|
+
dataType: "json";
|
|
205
|
+
columnType: "PgJsonb";
|
|
206
|
+
data: unknown;
|
|
207
|
+
driverParam: unknown;
|
|
208
|
+
notNull: false;
|
|
209
|
+
hasDefault: false;
|
|
210
|
+
isPrimaryKey: false;
|
|
211
|
+
isAutoincrement: false;
|
|
212
|
+
hasRuntimeDefault: false;
|
|
213
|
+
enumValues: undefined;
|
|
214
|
+
baseColumn: never;
|
|
215
|
+
identity: undefined;
|
|
216
|
+
generated: undefined;
|
|
217
|
+
}, {}, {}>;
|
|
218
|
+
createdAt: import("drizzle-orm/pg-core", { with: { "resolution-mode": "require" } }).PgColumn<{
|
|
219
|
+
name: "created_at";
|
|
220
|
+
tableName: "shop_invoices";
|
|
221
|
+
dataType: "date";
|
|
222
|
+
columnType: "PgTimestamp";
|
|
223
|
+
data: Date;
|
|
224
|
+
driverParam: string;
|
|
225
|
+
notNull: true;
|
|
226
|
+
hasDefault: true;
|
|
227
|
+
isPrimaryKey: false;
|
|
228
|
+
isAutoincrement: false;
|
|
229
|
+
hasRuntimeDefault: false;
|
|
230
|
+
enumValues: undefined;
|
|
231
|
+
baseColumn: never;
|
|
232
|
+
identity: undefined;
|
|
233
|
+
generated: undefined;
|
|
234
|
+
}, {}, {}>;
|
|
235
|
+
updatedAt: import("drizzle-orm/pg-core", { with: { "resolution-mode": "require" } }).PgColumn<{
|
|
236
|
+
name: "updated_at";
|
|
237
|
+
tableName: "shop_invoices";
|
|
238
|
+
dataType: "date";
|
|
239
|
+
columnType: "PgTimestamp";
|
|
240
|
+
data: Date;
|
|
241
|
+
driverParam: string;
|
|
242
|
+
notNull: true;
|
|
243
|
+
hasDefault: true;
|
|
244
|
+
isPrimaryKey: false;
|
|
245
|
+
isAutoincrement: false;
|
|
246
|
+
hasRuntimeDefault: false;
|
|
247
|
+
enumValues: undefined;
|
|
248
|
+
baseColumn: never;
|
|
249
|
+
identity: undefined;
|
|
250
|
+
generated: undefined;
|
|
251
|
+
}, {}, {}>;
|
|
252
|
+
};
|
|
253
|
+
dialect: "pg";
|
|
254
|
+
}>;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { integer, jsonb, pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core';
|
|
2
|
+
import { shopOrdersTable } from './order.js';
|
|
3
|
+
/**
|
|
4
|
+
* One invoice per order (`order_id` is unique → idempotency guard). `external_id`
|
|
5
|
+
* / `number` / `pdf_url` come from the invoicing provider once issued. The
|
|
6
|
+
* `attempts` / `next_retry_at` columns are reserved for a future automatic-retry
|
|
7
|
+
* mechanism; the current flow only retries manually from the admin.
|
|
8
|
+
*/
|
|
9
|
+
export const shopInvoicesTable = pgTable('shop_invoices', {
|
|
10
|
+
id: uuid('id').primaryKey().defaultRandom(),
|
|
11
|
+
orderId: uuid('order_id')
|
|
12
|
+
.notNull()
|
|
13
|
+
.unique()
|
|
14
|
+
.references(() => shopOrdersTable.id, { onDelete: 'cascade' }),
|
|
15
|
+
provider: text('provider').notNull(),
|
|
16
|
+
externalId: text('external_id'),
|
|
17
|
+
number: text('number'),
|
|
18
|
+
pdfUrl: text('pdf_url'),
|
|
19
|
+
kind: text('kind').notNull().default('vat'),
|
|
20
|
+
status: text('status').$type().notNull().default('pending'),
|
|
21
|
+
attempts: integer('attempts').notNull().default(0),
|
|
22
|
+
nextRetryAt: timestamp('next_retry_at', { withTimezone: true }),
|
|
23
|
+
lastError: text('last_error'),
|
|
24
|
+
raw: jsonb('raw'),
|
|
25
|
+
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
|
26
|
+
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull()
|
|
27
|
+
});
|