includio-cms 0.28.0 → 0.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/API.md +39 -13
  2. package/CHANGELOG.md +19 -0
  3. package/DOCS.md +1 -1
  4. package/ROADMAP.md +1 -0
  5. package/dist/admin/api/handler.js +4 -0
  6. package/dist/admin/api/integrations.d.ts +13 -0
  7. package/dist/admin/api/integrations.js +61 -0
  8. package/dist/admin/api/test-email.d.ts +9 -0
  9. package/dist/admin/api/test-email.js +39 -0
  10. package/dist/admin/auth-client.d.ts +2209 -2209
  11. package/dist/admin/client/index.d.ts +10 -0
  12. package/dist/admin/client/index.js +12 -0
  13. package/dist/admin/client/maintenance/maintenance-page.svelte +210 -0
  14. package/dist/admin/client/shop/coupon-schema.d.ts +1 -1
  15. package/dist/admin/client/shop/restore-order-cell.svelte +29 -0
  16. package/dist/admin/client/shop/restore-order-cell.svelte.d.ts +8 -0
  17. package/dist/admin/client/shop/shop-order-detail-page.svelte +71 -1
  18. package/dist/admin/client/shop/shop-orders-list-page.svelte +113 -53
  19. package/dist/admin/components/layout/app-sidebar.svelte +2 -0
  20. package/dist/admin/components/layout/nav-custom.svelte +26 -0
  21. package/dist/admin/components/layout/nav-custom.svelte.d.ts +3 -0
  22. package/dist/admin/components/layout/page-header.svelte +13 -3
  23. package/dist/admin/components/layout/page-header.svelte.d.ts +13 -3
  24. package/dist/admin/remote/admin.remote.d.ts +7 -0
  25. package/dist/admin/remote/admin.remote.js +10 -0
  26. package/dist/admin/remote/entry.remote.d.ts +4 -4
  27. package/dist/admin/remote/index.d.ts +1 -0
  28. package/dist/admin/remote/index.js +1 -0
  29. package/dist/admin/remote/invite.d.ts +2 -2
  30. package/dist/admin/remote/shop.remote.d.ts +75 -48
  31. package/dist/admin/remote/shop.remote.js +41 -10
  32. package/dist/admin/types.d.ts +15 -0
  33. package/dist/admin/utils/csv-export.d.ts +45 -0
  34. package/dist/admin/utils/csv-export.js +61 -0
  35. package/dist/cli/scaffold/admin.js +1 -1
  36. package/dist/components/ui/button-group/button-group-separator.svelte.d.ts +1 -1
  37. package/dist/components/ui/command/command.svelte.d.ts +1 -1
  38. package/dist/components/ui/field/field-label.svelte.d.ts +1 -1
  39. package/dist/components/ui/input/input.svelte.d.ts +1 -1
  40. package/dist/components/ui/input-group/input-group-input.svelte.d.ts +1 -1
  41. package/dist/components/ui/item/item-separator.svelte.d.ts +1 -1
  42. package/dist/components/ui/select/select-group-heading.svelte.d.ts +1 -1
  43. package/dist/components/ui/sidebar/sidebar-input.svelte.d.ts +1 -1
  44. package/dist/components/ui/sidebar/sidebar-separator.svelte.d.ts +1 -1
  45. package/dist/core/cms.d.ts +44 -2
  46. package/dist/core/cms.js +64 -0
  47. package/dist/core/index.d.ts +1 -4
  48. package/dist/core/index.js +4 -4
  49. package/dist/core/server/index.d.ts +4 -1
  50. package/dist/core/server/index.js +4 -1
  51. package/dist/db-postgres/schema/shop/order.d.ts +34 -0
  52. package/dist/db-postgres/schema/shop/order.js +4 -0
  53. package/dist/paraglide/messages/_index.d.ts +3 -36
  54. package/dist/paraglide/messages/_index.js +3 -71
  55. package/dist/paraglide/messages/hello_world.d.ts +5 -0
  56. package/dist/paraglide/messages/hello_world.js +33 -0
  57. package/dist/paraglide/messages/login_hello.d.ts +16 -0
  58. package/dist/paraglide/messages/login_hello.js +34 -0
  59. package/dist/paraglide/messages/login_please_login.d.ts +16 -0
  60. package/dist/paraglide/messages/login_please_login.js +34 -0
  61. package/dist/shop/adapters/fakturownia/client.d.ts +5 -0
  62. package/dist/shop/adapters/fakturownia/client.js +20 -0
  63. package/dist/shop/adapters/fakturownia/index.js +11 -0
  64. package/dist/shop/adapters/payu/index.js +11 -0
  65. package/dist/shop/index.d.ts +1 -1
  66. package/dist/shop/server/coupons.d.ts +10 -0
  67. package/dist/shop/server/coupons.js +19 -0
  68. package/dist/shop/server/email.d.ts +7 -3
  69. package/dist/shop/server/email.js +86 -112
  70. package/dist/shop/server/emailTemplateRegistry.d.ts +47 -0
  71. package/dist/shop/server/emailTemplateRegistry.js +288 -0
  72. package/dist/shop/server/orders.d.ts +60 -1
  73. package/dist/shop/server/orders.js +145 -16
  74. package/dist/shop/templates/_partials/footer.en.html +4 -0
  75. package/dist/shop/templates/_partials/footer.pl.html +4 -0
  76. package/dist/shop/templates/_partials/header.en.html +4 -0
  77. package/dist/shop/templates/_partials/header.pl.html +4 -0
  78. package/dist/shop/templates/_partials/items.en.html +14 -0
  79. package/dist/shop/templates/_partials/items.pl.html +14 -0
  80. package/dist/shop/templates/_partials/tracking.en.html +7 -0
  81. package/dist/shop/templates/_partials/tracking.pl.html +7 -0
  82. package/dist/shop/templates/awaiting-payment.en.html +6 -0
  83. package/dist/shop/templates/awaiting-payment.pl.html +6 -0
  84. package/dist/shop/templates/cancelled.en.html +6 -0
  85. package/dist/shop/templates/cancelled.pl.html +6 -0
  86. package/dist/shop/templates/low-stock.en.html +14 -0
  87. package/dist/shop/templates/low-stock.pl.html +14 -0
  88. package/dist/shop/templates/order-completed.en.html +6 -0
  89. package/dist/shop/templates/order-completed.pl.html +6 -0
  90. package/dist/shop/templates/order-received.en.html +7 -0
  91. package/dist/shop/templates/order-received.pl.html +7 -0
  92. package/dist/shop/templates/payment-received.en.html +7 -0
  93. package/dist/shop/templates/payment-received.pl.html +7 -0
  94. package/dist/shop/templates/payment-rejected.en.html +6 -0
  95. package/dist/shop/templates/payment-rejected.pl.html +6 -0
  96. package/dist/shop/templates/preparing.en.html +7 -0
  97. package/dist/shop/templates/preparing.pl.html +7 -0
  98. package/dist/shop/templates/refunded.en.html +6 -0
  99. package/dist/shop/templates/refunded.pl.html +6 -0
  100. package/dist/shop/templates/shipped.en.html +7 -0
  101. package/dist/shop/templates/shipped.pl.html +7 -0
  102. package/dist/shop/types.d.ts +63 -0
  103. package/dist/sveltekit/index.d.ts +0 -1
  104. package/dist/sveltekit/index.js +0 -1
  105. package/dist/sveltekit/server/index.d.ts +2 -0
  106. package/dist/sveltekit/server/index.js +4 -0
  107. package/dist/types/adapters/email.d.ts +13 -0
  108. package/dist/types/cms.d.ts +30 -0
  109. package/dist/types/index.d.ts +1 -1
  110. package/dist/updates/0.34.0/index.d.ts +2 -0
  111. package/dist/updates/0.34.0/index.js +17 -0
  112. package/dist/updates/index.js +3 -1
  113. package/package.json +7 -2
  114. package/dist/paraglide/messages/en.d.ts +0 -5
  115. package/dist/paraglide/messages/en.js +0 -14
  116. package/dist/paraglide/messages/pl.d.ts +0 -5
  117. package/dist/paraglide/messages/pl.js +0 -14
@@ -1,4 +1,4 @@
1
- import { and, asc, desc, eq, inArray, isNull, lt, sql } from 'drizzle-orm';
1
+ import { and, asc, desc, eq, ilike, inArray, isNotNull, isNull, lt, or, sql } from 'drizzle-orm';
2
2
  import { shopOrderItemsTable, shopOrderStatusHistoryTable, shopOrdersTable, shopProductVariantsTable, shopProductsTable, shopShippingMethodsTable, shopStockReservationsTable } from '../../db-postgres/schema/shop/index.js';
3
3
  import { getCMS } from '../../core/cms.js';
4
4
  import { getShopDb, requireShopConfig } from './db.js';
@@ -9,7 +9,7 @@ import { sendLowStockEmail, sendOrderStatusEmail } from './email.js';
9
9
  import { isPaymentMethodAllowed } from './payment-compat.js';
10
10
  import { isVariantExpired, VariantExpiredError } from '../expiry.js';
11
11
  import { resolvePaymentAmount } from './payment-policy.js';
12
- import { maybeIssueInvoiceForOrder } from './invoices.js';
12
+ import { getInvoiceByOrderId, maybeIssueInvoiceForOrder } from './invoices.js';
13
13
  import { CouponError, recordCouponRedemption, releaseCouponSlot, reserveCouponSlot, validateCoupon } from './coupons.js';
14
14
  /**
15
15
  * @public
@@ -27,6 +27,55 @@ export class MixedPaymentPolicyError extends Error {
27
27
  }
28
28
  }
29
29
  const STOCK_RESERVATION_TTL_MINUTES = 30;
30
+ /**
31
+ * @public
32
+ * Order statuses an admin is allowed to soft-delete (hide from the admin list).
33
+ * Restricted to states that never carry a settled payment or an issued invoice,
34
+ * so hiding one can't bury accounting/audit data. Paid-or-later and `refunded`
35
+ * orders are never deletable. The invoice guard in `softDeleteOrder` is the
36
+ * second line of defence.
37
+ */
38
+ export const DELETABLE_ORDER_STATUSES = new Set([
39
+ 'new',
40
+ 'awaitingPayment',
41
+ 'cancelled',
42
+ 'paymentRejected'
43
+ ]);
44
+ /** @public Pure status-level deletability check (no DB / invoice lookup). */
45
+ export function isOrderDeletable(status) {
46
+ return DELETABLE_ORDER_STATUSES.has(status);
47
+ }
48
+ /**
49
+ * @public
50
+ * Pure decision: may this order be soft-deleted? Encodes both guards (status +
51
+ * existing invoice) so they're testable without a DB. `invoice` is the order's
52
+ * current invoice record (or null). An `issued`/`sent` invoice hard-blocks the
53
+ * delete; `pending`/`failed` invoices don't (no legal document was produced).
54
+ */
55
+ export function decideOrderDeletion(status, invoice) {
56
+ if (!isOrderDeletable(status))
57
+ return { ok: false, reason: 'status' };
58
+ if (invoice && (invoice.status === 'issued' || invoice.status === 'sent')) {
59
+ return { ok: false, reason: 'invoice' };
60
+ }
61
+ return { ok: true };
62
+ }
63
+ /**
64
+ * @public
65
+ * Thrown by `softDeleteOrder` when the order can't be hidden: either its status
66
+ * isn't in {@link DELETABLE_ORDER_STATUSES} or it already has an issued invoice.
67
+ */
68
+ export class OrderNotDeletableError extends Error {
69
+ reason;
70
+ code = 'ORDER_NOT_DELETABLE';
71
+ constructor(reason) {
72
+ super(reason === 'invoice'
73
+ ? 'Order has an issued invoice and cannot be deleted.'
74
+ : 'Order status does not allow deletion.');
75
+ this.reason = reason;
76
+ this.name = 'OrderNotDeletableError';
77
+ }
78
+ }
30
79
  async function purgeExpiredReservations() {
31
80
  const db = getShopDb();
32
81
  await db
@@ -361,9 +410,18 @@ export async function updateOrderStatus(orderId, status, opts = {}) {
361
410
  })
362
411
  .where(eq(shopOrdersTable.id, orderId));
363
412
  }
413
+ // Auto-restore: a soft-deleted order may only sit in a deletable status. If
414
+ // it transitions out of that set (e.g. a late payment webhook flips a hidden
415
+ // `awaitingPayment` order to `paid`), un-hide it so settled orders are never
416
+ // buried in the trash.
417
+ const autoRestore = order.deletedAt != null && !isOrderDeletable(status);
364
418
  await db
365
419
  .update(shopOrdersTable)
366
- .set({ status, updatedAt: new Date() })
420
+ .set({
421
+ status,
422
+ updatedAt: new Date(),
423
+ ...(autoRestore ? { deletedAt: null, deletedBy: null } : {})
424
+ })
367
425
  .where(eq(shopOrdersTable.id, orderId));
368
426
  await db.insert(shopOrderStatusHistoryTable).values({
369
427
  orderId,
@@ -439,6 +497,14 @@ export async function updateOrderStatus(orderId, status, opts = {}) {
439
497
  // the guard inside skips deposit orders that still owe a balance.
440
498
  if (status === 'paid')
441
499
  void maybeIssueInvoiceForOrder(orderId);
500
+ // User-land `onOrderPaid` hook — fires on transition INTO `paid` only
501
+ // (no-op when oldStatus === 'paid'). Errors swallowed so a buggy callback
502
+ // never blocks the webhook / status write.
503
+ if (order.status !== 'paid' && status === 'paid' && shop.onOrderPaid && updated) {
504
+ Promise.resolve(shop.onOrderPaid(updated)).catch((e) => {
505
+ console.error('[onOrderPaid] callback failed', e);
506
+ });
507
+ }
442
508
  return updated;
443
509
  }
444
510
  /**
@@ -533,7 +599,12 @@ export async function getOrderById(id) {
533
599
  }
534
600
  export async function getOrderByNumber(number) {
535
601
  const db = getShopDb();
536
- const [row] = await db.select().from(shopOrdersTable).where(eq(shopOrdersTable.number, number));
602
+ // Customer-facing lookup a soft-deleted order must not resolve on the
603
+ // storefront. (Admin uses getOrderById, which intentionally ignores deletedAt.)
604
+ const [row] = await db
605
+ .select()
606
+ .from(shopOrdersTable)
607
+ .where(and(eq(shopOrdersTable.number, number), isNull(shopOrdersTable.deletedAt)));
537
608
  return row ?? null;
538
609
  }
539
610
  export async function getOrderItems(orderId) {
@@ -548,16 +619,28 @@ export async function getOrderStatusHistory(orderId) {
548
619
  .where(eq(shopOrderStatusHistoryTable.orderId, orderId))
549
620
  .orderBy(asc(shopOrderStatusHistoryTable.changedAt));
550
621
  }
551
- export async function listOrders(opts = {}) {
552
- const db = getShopDb();
622
+ function escapeLike(value) {
623
+ return value.replace(/[\\%_]/g, (m) => `\\${m}`);
624
+ }
625
+ function buildOrderListConditions(opts) {
553
626
  const conditions = [];
627
+ const deleted = opts.deleted ?? 'exclude';
628
+ if (deleted === 'exclude')
629
+ conditions.push(isNull(shopOrdersTable.deletedAt));
630
+ else if (deleted === 'only')
631
+ conditions.push(isNotNull(shopOrdersTable.deletedAt));
554
632
  if (opts.status)
555
633
  conditions.push(eq(shopOrdersTable.status, opts.status));
556
- if (opts.email)
557
- conditions.push(eq(shopOrdersTable.customerEmail, opts.email));
558
- // Exclude soft-deleted if such a column existed; none now
559
- // Avoid unused import warning
560
- void isNull;
634
+ const search = opts.search?.trim();
635
+ if (search) {
636
+ const pattern = `%${escapeLike(search)}%`;
637
+ conditions.push(or(ilike(shopOrdersTable.number, pattern), ilike(shopOrdersTable.customerEmail, pattern), ilike(shopOrdersTable.customerName, pattern)));
638
+ }
639
+ return conditions;
640
+ }
641
+ export async function listOrders(opts = {}) {
642
+ const db = getShopDb();
643
+ const conditions = buildOrderListConditions(opts);
561
644
  const where = conditions.length > 0 ? and(...conditions) : undefined;
562
645
  return db
563
646
  .select()
@@ -569,11 +652,7 @@ export async function listOrders(opts = {}) {
569
652
  }
570
653
  export async function countOrders(opts = {}) {
571
654
  const db = getShopDb();
572
- const conditions = [];
573
- if (opts.status)
574
- conditions.push(eq(shopOrdersTable.status, opts.status));
575
- if (opts.email)
576
- conditions.push(eq(shopOrdersTable.customerEmail, opts.email));
655
+ const conditions = buildOrderListConditions(opts);
577
656
  const where = conditions.length > 0 ? and(...conditions) : undefined;
578
657
  const [row] = await db
579
658
  .select({ count: sql `count(*)::int` })
@@ -581,3 +660,53 @@ export async function countOrders(opts = {}) {
581
660
  .where(where);
582
661
  return row?.count ?? 0;
583
662
  }
663
+ /**
664
+ * @public
665
+ * Soft-delete an order: hide it from the admin/customer list without removing
666
+ * the row (accounting/audit safety). Idempotent — a no-op on an already-deleted
667
+ * order. Guards on {@link decideOrderDeletion}: throws {@link OrderNotDeletableError}
668
+ * when the status isn't deletable or an issued/sent invoice exists. Releases any
669
+ * active stock reservation immediately so a hidden, abandoned order never locks
670
+ * stock waiting for the TTL.
671
+ */
672
+ export async function softDeleteOrder(orderId, deletedBy) {
673
+ const db = getShopDb();
674
+ const [order] = await db.select().from(shopOrdersTable).where(eq(shopOrdersTable.id, orderId));
675
+ if (!order)
676
+ throw new Error('Order not found');
677
+ if (order.deletedAt)
678
+ return order;
679
+ const invoice = await getInvoiceByOrderId(orderId);
680
+ const decision = decideOrderDeletion(order.status, invoice);
681
+ if (!decision.ok)
682
+ throw new OrderNotDeletableError(decision.reason);
683
+ // Free held stock right away (no rows when the stock feature is off).
684
+ await db
685
+ .delete(shopStockReservationsTable)
686
+ .where(eq(shopStockReservationsTable.orderId, orderId));
687
+ await db
688
+ .update(shopOrdersTable)
689
+ .set({ deletedAt: new Date(), deletedBy, updatedAt: new Date() })
690
+ .where(eq(shopOrdersTable.id, orderId));
691
+ const [updated] = await db.select().from(shopOrdersTable).where(eq(shopOrdersTable.id, orderId));
692
+ return updated;
693
+ }
694
+ /**
695
+ * @public
696
+ * Restore a soft-deleted order back to the visible list. Idempotent — a no-op
697
+ * on an order that isn't deleted.
698
+ */
699
+ export async function restoreOrder(orderId) {
700
+ const db = getShopDb();
701
+ const [order] = await db.select().from(shopOrdersTable).where(eq(shopOrdersTable.id, orderId));
702
+ if (!order)
703
+ throw new Error('Order not found');
704
+ if (!order.deletedAt)
705
+ return order;
706
+ await db
707
+ .update(shopOrdersTable)
708
+ .set({ deletedAt: null, deletedBy: null, updatedAt: new Date() })
709
+ .where(eq(shopOrdersTable.id, orderId));
710
+ const [updated] = await db.select().from(shopOrdersTable).where(eq(shopOrdersTable.id, orderId));
711
+ return updated;
712
+ }
@@ -0,0 +1,4 @@
1
+ </div>
2
+ <p style="text-align:center;color:#8888A0;font-size:12px;margin-top:16px;">AriaCMS · {{order.customerEmail}}</p>
3
+ </div>
4
+ </body></html>
@@ -0,0 +1,4 @@
1
+ </div>
2
+ <p style="text-align:center;color:#8888A0;font-size:12px;margin-top:16px;">AriaCMS · {{order.customerEmail}}</p>
3
+ </div>
4
+ </body></html>
@@ -0,0 +1,4 @@
1
+ <!doctype html>
2
+ <html lang="en"><body style="margin:0;padding:0;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif;background:#f5f5f8;color:#1a1a2e;">
3
+ <div style="max-width:560px;margin:0 auto;padding:24px;">
4
+ <div style="background:#fff;border-radius:12px;padding:28px;">
@@ -0,0 +1,4 @@
1
+ <!doctype html>
2
+ <html lang="pl"><body style="margin:0;padding:0;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif;background:#f5f5f8;color:#1a1a2e;">
3
+ <div style="max-width:560px;margin:0 auto;padding:24px;">
4
+ <div style="background:#fff;border-radius:12px;padding:28px;">
@@ -0,0 +1,14 @@
1
+ <table style="width:100%;border-collapse:collapse;font-size:14px;">
2
+ <thead><tr style="background:#f4f2fa;"><th align="left" style="padding:8px;">Item</th><th style="padding:8px;">Qty</th><th align="right" style="padding:8px;">Total</th></tr></thead>
3
+ <tbody>
4
+ {{#each items}}
5
+ <tr><td style="padding:6px 8px;border-bottom:1px solid #eee;">{{name}}</td><td style="padding:6px 8px;border-bottom:1px solid #eee;text-align:center;">{{qty}}</td><td style="padding:6px 8px;border-bottom:1px solid #eee;text-align:right;">{{lineGross}}</td></tr>
6
+ {{/each}}
7
+ </tbody>
8
+ </table>
9
+ <div style="margin-top:16px;text-align:right;font-size:14px;line-height:1.7;">
10
+ <div>Shipping: <strong>{{order.shippingGross}}</strong></div>
11
+ {{#if order.hasDiscount}}<div>Discount ({{order.couponCode}}): <strong style="color:#5B4A9E;">−{{order.discountAmount}}</strong></div>{{/if}}
12
+ <div style="font-size:16px;">Total (gross): <strong style="color:#5B4A9E;">{{order.totalGross}}</strong></div>
13
+ <div style="color:#8888A0;font-size:12px;">net {{order.totalNet}} · VAT {{order.vatAmount}}</div>
14
+ </div>
@@ -0,0 +1,14 @@
1
+ <table style="width:100%;border-collapse:collapse;font-size:14px;">
2
+ <thead><tr style="background:#f4f2fa;"><th align="left" style="padding:8px;">Pozycja</th><th style="padding:8px;">Ilość</th><th align="right" style="padding:8px;">Suma</th></tr></thead>
3
+ <tbody>
4
+ {{#each items}}
5
+ <tr><td style="padding:6px 8px;border-bottom:1px solid #eee;">{{name}}</td><td style="padding:6px 8px;border-bottom:1px solid #eee;text-align:center;">{{qty}}</td><td style="padding:6px 8px;border-bottom:1px solid #eee;text-align:right;">{{lineGross}}</td></tr>
6
+ {{/each}}
7
+ </tbody>
8
+ </table>
9
+ <div style="margin-top:16px;text-align:right;font-size:14px;line-height:1.7;">
10
+ <div>Wysyłka: <strong>{{order.shippingGross}}</strong></div>
11
+ {{#if order.hasDiscount}}<div>Rabat ({{order.couponCode}}): <strong style="color:#5B4A9E;">−{{order.discountAmount}}</strong></div>{{/if}}
12
+ <div style="font-size:16px;">Razem (brutto): <strong style="color:#5B4A9E;">{{order.totalGross}}</strong></div>
13
+ <div style="color:#8888A0;font-size:12px;">netto {{order.totalNet}} · VAT {{order.vatAmount}}</div>
14
+ </div>
@@ -0,0 +1,7 @@
1
+ {{#if tracking}}
2
+ <div style="margin-top:20px;padding:16px;background:#F4F2FA;border-radius:10px;font-size:14px;">
3
+ <div style="color:#555566;margin-bottom:4px;">{{tracking.label}}</div>
4
+ <div style="font-family:ui-monospace,monospace;font-weight:700;word-break:break-all;">{{tracking.number}}</div>
5
+ {{#if tracking.url}}<a href="{{tracking.url}}" style="display:inline-block;margin-top:8px;color:#5B4A9E;font-weight:600;text-decoration:none;">{{tracking.linkLabel}}</a>{{/if}}
6
+ </div>
7
+ {{/if}}
@@ -0,0 +1,7 @@
1
+ {{#if tracking}}
2
+ <div style="margin-top:20px;padding:16px;background:#F4F2FA;border-radius:10px;font-size:14px;">
3
+ <div style="color:#555566;margin-bottom:4px;">{{tracking.label}}</div>
4
+ <div style="font-family:ui-monospace,monospace;font-weight:700;word-break:break-all;">{{tracking.number}}</div>
5
+ {{#if tracking.url}}<a href="{{tracking.url}}" style="display:inline-block;margin-top:8px;color:#5B4A9E;font-weight:600;text-decoration:none;">{{tracking.linkLabel}}</a>{{/if}}
6
+ </div>
7
+ {{/if}}
@@ -0,0 +1,6 @@
1
+ {{> header}}
2
+ <h1 style="font-size:20px;font-weight:800;margin:0 0 8px;">Awaiting payment</h1>
3
+ <p style="margin:0 0 20px;color:#555566;line-height:1.5;">Your order {{order.number}} has been placed. Waiting for payment per the chosen method.</p>
4
+ {{> items}}
5
+ {{#if viewUrl}}<div style="margin-top:24px;text-align:center;"><a href="{{viewUrl}}" style="display:inline-block;background:#5B4A9E;color:#fff;text-decoration:none;padding:10px 18px;border-radius:8px;font-weight:600;">{{viewLinkLabel}}</a></div>{{/if}}
6
+ {{> footer}}
@@ -0,0 +1,6 @@
1
+ {{> header}}
2
+ <h1 style="font-size:20px;font-weight:800;margin:0 0 8px;">Czekamy na płatność</h1>
3
+ <p style="margin:0 0 20px;color:#555566;line-height:1.5;">Otrzymaliśmy zamówienie {{order.number}}. Czekamy na płatność zgodnie z wybraną metodą.</p>
4
+ {{> items}}
5
+ {{#if viewUrl}}<div style="margin-top:24px;text-align:center;"><a href="{{viewUrl}}" style="display:inline-block;background:#5B4A9E;color:#fff;text-decoration:none;padding:10px 18px;border-radius:8px;font-weight:600;">{{viewLinkLabel}}</a></div>{{/if}}
6
+ {{> footer}}
@@ -0,0 +1,6 @@
1
+ {{> header}}
2
+ <h1 style="font-size:20px;font-weight:800;margin:0 0 8px;">Order cancelled</h1>
3
+ <p style="margin:0 0 20px;color:#555566;line-height:1.5;">Order {{order.number}} has been cancelled.</p>
4
+ {{> items}}
5
+ {{#if viewUrl}}<div style="margin-top:24px;text-align:center;"><a href="{{viewUrl}}" style="display:inline-block;background:#5B4A9E;color:#fff;text-decoration:none;padding:10px 18px;border-radius:8px;font-weight:600;">{{viewLinkLabel}}</a></div>{{/if}}
6
+ {{> footer}}
@@ -0,0 +1,6 @@
1
+ {{> header}}
2
+ <h1 style="font-size:20px;font-weight:800;margin:0 0 8px;">Zamówienie anulowane</h1>
3
+ <p style="margin:0 0 20px;color:#555566;line-height:1.5;">Zamówienie {{order.number}} zostało anulowane.</p>
4
+ {{> items}}
5
+ {{#if viewUrl}}<div style="margin-top:24px;text-align:center;"><a href="{{viewUrl}}" style="display:inline-block;background:#5B4A9E;color:#fff;text-decoration:none;padding:10px 18px;border-radius:8px;font-weight:600;">{{viewLinkLabel}}</a></div>{{/if}}
6
+ {{> footer}}
@@ -0,0 +1,14 @@
1
+ <!doctype html>
2
+ <html lang="en"><body style="margin:0;padding:0;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif;background:#f5f5f8;color:#1a1a2e;">
3
+ <div style="max-width:560px;margin:0 auto;padding:24px;">
4
+ <div style="background:#fff;border-radius:12px;padding:28px;">
5
+ <h1 style="font-size:20px;font-weight:800;margin:0 0 8px;color:#C4893A;">Low stock alert</h1>
6
+ <p style="margin:0 0 16px;color:#555566;line-height:1.5;">
7
+ Product <strong>{{product.title}}</strong>{{#if product.variantSku}} (SKU {{product.variantSku}}){{/if}}
8
+ has only <strong>{{product.stock}}</strong> units left in stock (alert threshold: {{product.threshold}}).
9
+ </p>
10
+ <p style="margin:0;color:#8888A0;font-size:13px;">Restock in the admin panel to avoid running out.</p>
11
+ </div>
12
+ <p style="text-align:center;color:#8888A0;font-size:12px;margin-top:16px;">AriaCMS · {{shop.adminEmail}}</p>
13
+ </div>
14
+ </body></html>
@@ -0,0 +1,14 @@
1
+ <!doctype html>
2
+ <html lang="pl"><body style="margin:0;padding:0;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif;background:#f5f5f8;color:#1a1a2e;">
3
+ <div style="max-width:560px;margin:0 auto;padding:24px;">
4
+ <div style="background:#fff;border-radius:12px;padding:28px;">
5
+ <h1 style="font-size:20px;font-weight:800;margin:0 0 8px;color:#C4893A;">Niski stan magazynowy</h1>
6
+ <p style="margin:0 0 16px;color:#555566;line-height:1.5;">
7
+ Produkt <strong>{{product.title}}</strong>{{#if product.variantSku}} (SKU {{product.variantSku}}){{/if}}
8
+ ma już tylko <strong>{{product.stock}}</strong> sztuk w magazynie (próg alertu: {{product.threshold}}).
9
+ </p>
10
+ <p style="margin:0;color:#8888A0;font-size:13px;">Uzupełnij stan w panelu admina, żeby uniknąć przerwy w sprzedaży.</p>
11
+ </div>
12
+ <p style="text-align:center;color:#8888A0;font-size:12px;margin-top:16px;">AriaCMS · {{shop.adminEmail}}</p>
13
+ </div>
14
+ </body></html>
@@ -0,0 +1,6 @@
1
+ {{> header}}
2
+ <h1 style="font-size:20px;font-weight:800;margin:0 0 8px;">Order completed</h1>
3
+ <p style="margin:0 0 20px;color:#555566;line-height:1.5;">Order {{order.number}} is complete. Thanks for shopping with us!</p>
4
+ {{> items}}
5
+ {{#if viewUrl}}<div style="margin-top:24px;text-align:center;"><a href="{{viewUrl}}" style="display:inline-block;background:#5B4A9E;color:#fff;text-decoration:none;padding:10px 18px;border-radius:8px;font-weight:600;">{{viewLinkLabel}}</a></div>{{/if}}
6
+ {{> footer}}
@@ -0,0 +1,6 @@
1
+ {{> header}}
2
+ <h1 style="font-size:20px;font-weight:800;margin:0 0 8px;">Zamówienie zrealizowane</h1>
3
+ <p style="margin:0 0 20px;color:#555566;line-height:1.5;">Zamówienie {{order.number}} zostało zakończone. Dziękujemy za zakupy!</p>
4
+ {{> items}}
5
+ {{#if viewUrl}}<div style="margin-top:24px;text-align:center;"><a href="{{viewUrl}}" style="display:inline-block;background:#5B4A9E;color:#fff;text-decoration:none;padding:10px 18px;border-radius:8px;font-weight:600;">{{viewLinkLabel}}</a></div>{{/if}}
6
+ {{> footer}}
@@ -0,0 +1,7 @@
1
+ {{> header}}
2
+ <h1 style="font-size:20px;font-weight:800;margin:0 0 8px;">Order {{order.number}}</h1>
3
+ <p style="margin:0 0 20px;color:#555566;line-height:1.5;">Thanks! We received your order and will process it shortly.</p>
4
+ {{> items}}
5
+ {{> tracking}}
6
+ {{#if viewUrl}}<div style="margin-top:24px;text-align:center;"><a href="{{viewUrl}}" style="display:inline-block;background:#5B4A9E;color:#fff;text-decoration:none;padding:10px 18px;border-radius:8px;font-weight:600;">{{viewLinkLabel}}</a></div>{{/if}}
7
+ {{> footer}}
@@ -0,0 +1,7 @@
1
+ {{> header}}
2
+ <h1 style="font-size:20px;font-weight:800;margin:0 0 8px;">Zamówienie {{order.number}}</h1>
3
+ <p style="margin:0 0 20px;color:#555566;line-height:1.5;">Dziękujemy! Otrzymaliśmy Twoje zamówienie i wkrótce je przetworzymy.</p>
4
+ {{> items}}
5
+ {{> tracking}}
6
+ {{#if viewUrl}}<div style="margin-top:24px;text-align:center;"><a href="{{viewUrl}}" style="display:inline-block;background:#5B4A9E;color:#fff;text-decoration:none;padding:10px 18px;border-radius:8px;font-weight:600;">{{viewLinkLabel}}</a></div>{{/if}}
7
+ {{> footer}}
@@ -0,0 +1,7 @@
1
+ {{> header}}
2
+ <h1 style="font-size:20px;font-weight:800;margin:0 0 8px;">Payment received</h1>
3
+ <p style="margin:0 0 20px;color:#555566;line-height:1.5;">Payment for order {{order.number}} has been received. Your order is moving to fulfilment.</p>
4
+ {{> items}}
5
+ {{> tracking}}
6
+ {{#if viewUrl}}<div style="margin-top:24px;text-align:center;"><a href="{{viewUrl}}" style="display:inline-block;background:#5B4A9E;color:#fff;text-decoration:none;padding:10px 18px;border-radius:8px;font-weight:600;">{{viewLinkLabel}}</a></div>{{/if}}
7
+ {{> footer}}
@@ -0,0 +1,7 @@
1
+ {{> header}}
2
+ <h1 style="font-size:20px;font-weight:800;margin:0 0 8px;">Płatność zaksięgowana</h1>
3
+ <p style="margin:0 0 20px;color:#555566;line-height:1.5;">Płatność za zamówienie {{order.number}} została zaksięgowana. Przekazujemy zamówienie do realizacji.</p>
4
+ {{> items}}
5
+ {{> tracking}}
6
+ {{#if viewUrl}}<div style="margin-top:24px;text-align:center;"><a href="{{viewUrl}}" style="display:inline-block;background:#5B4A9E;color:#fff;text-decoration:none;padding:10px 18px;border-radius:8px;font-weight:600;">{{viewLinkLabel}}</a></div>{{/if}}
7
+ {{> footer}}
@@ -0,0 +1,6 @@
1
+ {{> header}}
2
+ <h1 style="font-size:20px;font-weight:800;margin:0 0 8px;">Payment rejected</h1>
3
+ <p style="margin:0 0 20px;color:#555566;line-height:1.5;">Payment for order {{order.number}} was not received. Please contact us if you have questions.</p>
4
+ {{> items}}
5
+ {{#if viewUrl}}<div style="margin-top:24px;text-align:center;"><a href="{{viewUrl}}" style="display:inline-block;background:#5B4A9E;color:#fff;text-decoration:none;padding:10px 18px;border-radius:8px;font-weight:600;">{{viewLinkLabel}}</a></div>{{/if}}
6
+ {{> footer}}
@@ -0,0 +1,6 @@
1
+ {{> header}}
2
+ <h1 style="font-size:20px;font-weight:800;margin:0 0 8px;">Płatność odrzucona</h1>
3
+ <p style="margin:0 0 20px;color:#555566;line-height:1.5;">Płatność za zamówienie {{order.number}} nie została zaksięgowana. Skontaktuj się z nami w razie pytań.</p>
4
+ {{> items}}
5
+ {{#if viewUrl}}<div style="margin-top:24px;text-align:center;"><a href="{{viewUrl}}" style="display:inline-block;background:#5B4A9E;color:#fff;text-decoration:none;padding:10px 18px;border-radius:8px;font-weight:600;">{{viewLinkLabel}}</a></div>{{/if}}
6
+ {{> footer}}
@@ -0,0 +1,7 @@
1
+ {{> header}}
2
+ <h1 style="font-size:20px;font-weight:800;margin:0 0 8px;">Preparing order</h1>
3
+ <p style="margin:0 0 20px;color:#555566;line-height:1.5;">Your order {{order.number}} is being prepared.</p>
4
+ {{> items}}
5
+ {{> tracking}}
6
+ {{#if viewUrl}}<div style="margin-top:24px;text-align:center;"><a href="{{viewUrl}}" style="display:inline-block;background:#5B4A9E;color:#fff;text-decoration:none;padding:10px 18px;border-radius:8px;font-weight:600;">{{viewLinkLabel}}</a></div>{{/if}}
7
+ {{> footer}}
@@ -0,0 +1,7 @@
1
+ {{> header}}
2
+ <h1 style="font-size:20px;font-weight:800;margin:0 0 8px;">Zamówienie w przygotowaniu</h1>
3
+ <p style="margin:0 0 20px;color:#555566;line-height:1.5;">Twoje zamówienie {{order.number}} jest pakowane.</p>
4
+ {{> items}}
5
+ {{> tracking}}
6
+ {{#if viewUrl}}<div style="margin-top:24px;text-align:center;"><a href="{{viewUrl}}" style="display:inline-block;background:#5B4A9E;color:#fff;text-decoration:none;padding:10px 18px;border-radius:8px;font-weight:600;">{{viewLinkLabel}}</a></div>{{/if}}
7
+ {{> footer}}
@@ -0,0 +1,6 @@
1
+ {{> header}}
2
+ <h1 style="font-size:20px;font-weight:800;margin:0 0 8px;">Order refunded</h1>
3
+ <p style="margin:0 0 20px;color:#555566;line-height:1.5;">Your refund for order {{order.number}} has been issued. It should reach your account within a few business days.</p>
4
+ {{> items}}
5
+ {{#if viewUrl}}<div style="margin-top:24px;text-align:center;"><a href="{{viewUrl}}" style="display:inline-block;background:#5B4A9E;color:#fff;text-decoration:none;padding:10px 18px;border-radius:8px;font-weight:600;">{{viewLinkLabel}}</a></div>{{/if}}
6
+ {{> footer}}
@@ -0,0 +1,6 @@
1
+ {{> header}}
2
+ <h1 style="font-size:20px;font-weight:800;margin:0 0 8px;">Zwrot zaksięgowany</h1>
3
+ <p style="margin:0 0 20px;color:#555566;line-height:1.5;">Środki za zamówienie {{order.number}} zostały zwrócone. Powinny pojawić się na koncie w ciągu kilku dni roboczych.</p>
4
+ {{> items}}
5
+ {{#if viewUrl}}<div style="margin-top:24px;text-align:center;"><a href="{{viewUrl}}" style="display:inline-block;background:#5B4A9E;color:#fff;text-decoration:none;padding:10px 18px;border-radius:8px;font-weight:600;">{{viewLinkLabel}}</a></div>{{/if}}
6
+ {{> footer}}
@@ -0,0 +1,7 @@
1
+ {{> header}}
2
+ <h1 style="font-size:20px;font-weight:800;margin:0 0 8px;">Order shipped</h1>
3
+ <p style="margin:0 0 20px;color:#555566;line-height:1.5;">Order {{order.number}} has been shipped.</p>
4
+ {{> items}}
5
+ {{> tracking}}
6
+ {{#if viewUrl}}<div style="margin-top:24px;text-align:center;"><a href="{{viewUrl}}" style="display:inline-block;background:#5B4A9E;color:#fff;text-decoration:none;padding:10px 18px;border-radius:8px;font-weight:600;">{{viewLinkLabel}}</a></div>{{/if}}
7
+ {{> footer}}
@@ -0,0 +1,7 @@
1
+ {{> header}}
2
+ <h1 style="font-size:20px;font-weight:800;margin:0 0 8px;">Zamówienie wysłane</h1>
3
+ <p style="margin:0 0 20px;color:#555566;line-height:1.5;">Zamówienie {{order.number}} zostało wysłane.</p>
4
+ {{> items}}
5
+ {{> tracking}}
6
+ {{#if viewUrl}}<div style="margin-top:24px;text-align:center;"><a href="{{viewUrl}}" style="display:inline-block;background:#5B4A9E;color:#fff;text-decoration:none;padding:10px 18px;border-radius:8px;font-weight:600;">{{viewLinkLabel}}</a></div>{{/if}}
7
+ {{> footer}}
@@ -1,5 +1,12 @@
1
1
  import type { Language } from '../types/languages.js';
2
+ import type { shopOrdersTable } from '../db-postgres/schema/shop/order.js';
2
3
  export type Currency = 'PLN';
4
+ /**
5
+ * Public row type for a single shop order — mirrors the Drizzle `shop_orders`
6
+ * table. Use this in `ShopConfig.onOrderPaid` and similar hooks.
7
+ * @public
8
+ */
9
+ export type Order = typeof shopOrdersTable.$inferSelect;
3
10
  export type OrderStatus = 'new' | 'awaitingPayment' | 'paid' | 'preparing' | 'sent' | 'done' | 'cancelled' | 'paymentRejected' | 'refunded';
4
11
  export type I18nText = {
5
12
  [lang: string]: string;
@@ -27,11 +34,28 @@ export interface PaymentCreateContext {
27
34
  customerIp?: string;
28
35
  language?: string | null;
29
36
  }
37
+ /**
38
+ * Result of a non-invasive integration connectivity check. Returned by an
39
+ * adapter's optional `healthCheck()` — `ok` reflects reachability + auth, and
40
+ * `message` carries a human-readable detail (error text on failure, optional
41
+ * note on success). MUST NOT leak secrets.
42
+ * @public
43
+ */
44
+ export interface IntegrationHealthResult {
45
+ ok: boolean;
46
+ message?: string;
47
+ }
30
48
  export interface PaymentAdapter {
31
49
  id: string;
32
50
  label: I18nText;
33
51
  createPayment(order: OrderRef, ctx?: PaymentCreateContext): Promise<PaymentCreateResult>;
34
52
  handleWebhook?(req: Request): Promise<PaymentEvent>;
53
+ /**
54
+ * Non-invasive connectivity check (e.g. an OAuth token fetch). Optional —
55
+ * adapters omitting this are reported as "health-check unsupported". MUST
56
+ * NOT create real transactions.
57
+ */
58
+ healthCheck?(): Promise<IntegrationHealthResult>;
35
59
  /**
36
60
  * Poll the provider for the latest payment status. Used as a fallback
37
61
  * when a webhook is lost. `providerRef` is the external id the adapter
@@ -323,6 +347,12 @@ export interface InvoicingAdapter {
323
347
  issueWhen?: InvoiceIssuePolicy;
324
348
  createInvoice(payload: InvoicePayload, ctx?: InvoiceContext): Promise<InvoiceCreateResult>;
325
349
  send?(externalId: string, ctx?: InvoiceContext): Promise<void>;
350
+ /**
351
+ * Non-invasive connectivity check (e.g. a read-only account lookup).
352
+ * Optional — adapters omitting this are reported as "health-check
353
+ * unsupported". MUST NOT issue real invoices.
354
+ */
355
+ healthCheck?(): Promise<IntegrationHealthResult>;
326
356
  }
327
357
  export interface ShopConfig {
328
358
  currency: Currency;
@@ -370,6 +400,39 @@ export interface ShopConfig {
370
400
  * @public
371
401
  */
372
402
  variantExpiry?: VariantExpiryConfig;
403
+ /**
404
+ * Optional shop email templates configuration. Templates are looked up
405
+ * first in the consumer project's `dir` (override), then in the bundled
406
+ * defaults. Files are named `<name>.<lang>.html` (per-lang) with a
407
+ * lang-agnostic project fallback (`<name>.html`). Handlebars syntax with
408
+ * partials in `_partials/<name>.<lang>.html`. CMS singletons accessible
409
+ * via `{{cms.<slug>.<field>}}` tokens (auto-fetched per template).
410
+ * @public
411
+ */
412
+ emailTemplates?: {
413
+ /** Project directory holding overrides. Default `'src/emails/shop'`. */
414
+ dir?: string;
415
+ /** Throw on missing tokens / unknown CMS slugs. Default `false` (lenient → ""). */
416
+ strict?: boolean;
417
+ };
418
+ /**
419
+ * Optional callback fired after an order transitions to `paid`.
420
+ *
421
+ * Triggered inside `updateOrderStatus` only when the previous status was
422
+ * NOT `paid` and the new status is `paid` (transition guard) — so a manual
423
+ * `cancelled → paid` flip will fire, but a no-op `paid → paid` call will not.
424
+ * Runs AFTER `sendOrderStatusEmail` and `maybeIssueInvoiceForOrder`; receives
425
+ * the fresh order row (post-update snapshot, with the latest `consents` etc).
426
+ *
427
+ * Errors are caught and logged — the callback never blocks the webhook,
428
+ * the status write, or the invoice. Use for fire-and-forget side effects
429
+ * (admin notifications, CRM sync, slack ping). If you need strict
430
+ * once-per-order semantics across refund→repay cycles, persist your own
431
+ * idempotency flag in userland (the transition guard alone fires once per
432
+ * `paid` entry, which may not match "lifetime once" for refunded orders).
433
+ * @public
434
+ */
435
+ onOrderPaid?: (order: Order) => Promise<void> | void;
373
436
  }
374
437
  export interface ResolvedShopConfig extends Omit<ShopConfig, 'variantLabel' | 'variantExpiry' | 'invoicing'> {
375
438
  features: Required<ShopFeatures>;
@@ -12,4 +12,3 @@ export { enableHybridEditing } from './components/hybrid-context.js';
12
12
  export { setPreferMp4 } from './components/video-context.js';
13
13
  export { getLink, isImageFieldData, isVideoFieldData } from './utils/index.js';
14
14
  export { structuredToHtml } from '../core/fields/structuredToHtml.js';
15
- export { extractBlocks, extractInlineBlocks, extractText, extractMediaRefs } from '../core/server/fields/queryStructuredContent.js';
@@ -12,4 +12,3 @@ export { enableHybridEditing } from './components/hybrid-context.js';
12
12
  export { setPreferMp4 } from './components/video-context.js';
13
13
  export { getLink, isImageFieldData, isVideoFieldData } from './utils/index.js';
14
14
  export { structuredToHtml } from '../core/fields/structuredToHtml.js';
15
- export { extractBlocks, extractInlineBlocks, extractText, extractMediaRefs } from '../core/server/fields/queryStructuredContent.js';
@@ -1,6 +1,7 @@
1
1
  export { includioCMS } from './handle.js';
2
2
  export { cmsLayoutLoad } from './layout.js';
3
3
  export { resolveEntry, resolveEntries, countEntries, type ResolveEntryOptions, type ResolveEntriesOptions, type CountEntriesOptions, type PopulateConfig, type ResolveStatus } from '../../core/server/entries/operations/resolveEntry.js';
4
+ export { getMailer, sendMail } from '../../core/cms.js';
4
5
  export { createFormSubmission } from '../../core/server/forms/submissions/operations/create.js';
5
6
  export { parseFormDataForSubmission } from '../../core/server/forms/submissions/utils/parseMultipart.js';
6
7
  export { createConsentLog } from '../../core/server/consentLogs/operations/create.js';
@@ -8,3 +9,4 @@ export { getPreviewEntry } from './preview.js';
8
9
  export { createRestApiHandler } from '../../admin/api/rest/handler.js';
9
10
  export { generateApiKey } from '../../admin/api/rest/middleware/generateApiKey.js';
10
11
  export { createAdminApiHandler } from '../../admin/api/handler.js';
12
+ export { extractBlocks, extractInlineBlocks, extractText, extractMediaRefs } from '../../core/server/fields/queryStructuredContent.js';
@@ -1,6 +1,9 @@
1
1
  export { includioCMS } from './handle.js';
2
2
  export { cmsLayoutLoad } from './layout.js';
3
3
  export { resolveEntry, resolveEntries, countEntries } from '../../core/server/entries/operations/resolveEntry.js';
4
+ // Moved from `includio-cms/core` (0.34.x): keep on the server entry so the
5
+ // client-safe `core` barrel (`resolveSeo`) no longer drags in the server graph.
6
+ export { getMailer, sendMail } from '../../core/cms.js';
4
7
  export { createFormSubmission } from '../../core/server/forms/submissions/operations/create.js';
5
8
  export { parseFormDataForSubmission } from '../../core/server/forms/submissions/utils/parseMultipart.js';
6
9
  export { createConsentLog } from '../../core/server/consentLogs/operations/create.js';
@@ -10,3 +13,4 @@ export { createRestApiHandler } from '../../admin/api/rest/handler.js';
10
13
  export { generateApiKey } from '../../admin/api/rest/middleware/generateApiKey.js';
11
14
  // Folded from `./admin/api/handler` (dropped as separate export in 0.26.1)
12
15
  export { createAdminApiHandler } from '../../admin/api/handler.js';
16
+ export { extractBlocks, extractInlineBlocks, extractText, extractMediaRefs } from '../../core/server/fields/queryStructuredContent.js';