@mohasinac/appkit 4.22.0 → 4.24.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 (112) hide show
  1. package/dist/_internal/server/features/bundles/actions.js +7 -0
  2. package/dist/_internal/server/features/checkout/actions.js +24 -16
  3. package/dist/_internal/server/features/checkout/locked-lines.js +12 -2
  4. package/dist/_internal/server/features/stores/adapters.d.ts +1 -1
  5. package/dist/_internal/server/features/stores/adapters.js +5 -0
  6. package/dist/_internal/server/jobs/core/auctionSettlement.js +10 -4
  7. package/dist/_internal/server/jobs/core/offerExpiry.js +10 -2
  8. package/dist/_internal/server/jobs/core/payoutBatch.js +15 -3
  9. package/dist/_internal/shared/actions/action-registry.js +41 -0
  10. package/dist/_internal/shared/checkout/order-math.d.ts +23 -5
  11. package/dist/_internal/shared/checkout/order-math.js +29 -6
  12. package/dist/_internal/shared/checkout/rules/_limits.d.ts +12 -2
  13. package/dist/_internal/shared/checkout/rules/_limits.js +12 -2
  14. package/dist/_internal/shared/features/cart/config.d.ts +2 -1
  15. package/dist/_internal/shared/features/cart/config.js +2 -1
  16. package/dist/_internal/shared/features/cart/schema.d.ts +6 -6
  17. package/dist/_internal/shared/features/notifications/action-url.d.ts +47 -0
  18. package/dist/_internal/shared/features/notifications/action-url.js +111 -0
  19. package/dist/_internal/shared/features/products/schema.d.ts +8 -8
  20. package/dist/_internal/shared/features/promotions/schema.d.ts +4 -4
  21. package/dist/_internal/shared/history/index.d.ts +1 -1
  22. package/dist/_internal/shared/history/types.d.ts +16 -0
  23. package/dist/client.d.ts +2 -0
  24. package/dist/client.js +1 -0
  25. package/dist/constants/field-names.d.ts +12 -0
  26. package/dist/constants/field-names.js +12 -0
  27. package/dist/constants/notification-types.d.ts +38 -10
  28. package/dist/constants/notification-types.js +39 -28
  29. package/dist/features/account/types/index.d.ts +1 -10
  30. package/dist/features/admin/actions/admin-actions.js +13 -3
  31. package/dist/features/admin/actions/notification-actions.d.ts +12 -0
  32. package/dist/features/admin/actions/notification-actions.js +149 -4
  33. package/dist/features/admin/components/AdminBidsView.js +7 -1
  34. package/dist/features/admin/components/AdminScammerEditorView.d.ts +5 -1
  35. package/dist/features/admin/components/AdminScammerEditorView.js +3 -2
  36. package/dist/features/admin/components/AdminSupportTicketDetailView.d.ts +5 -1
  37. package/dist/features/admin/components/AdminSupportTicketDetailView.js +3 -2
  38. package/dist/features/admin/schemas/firestore.d.ts +14 -3
  39. package/dist/features/admin/schemas/firestore.js +79 -0
  40. package/dist/features/auctions/repository/bid.repository.d.ts +21 -8
  41. package/dist/features/auctions/repository/bid.repository.js +57 -33
  42. package/dist/features/auctions/schemas/firestore.d.ts +27 -0
  43. package/dist/features/auctions/schemas/firestore.js +21 -0
  44. package/dist/features/auth/schemas/index.d.ts +2 -2
  45. package/dist/features/cart/actions/cart-actions.d.ts +16 -0
  46. package/dist/features/cart/actions/cart-actions.js +26 -12
  47. package/dist/features/cart/components/CartDrawer.js +2 -1
  48. package/dist/features/cart/schemas/firestore.d.ts +132 -8
  49. package/dist/features/cart/schemas/firestore.js +22 -0
  50. package/dist/features/catalogue/actions/approve-catalogue-listing.js +7 -6
  51. package/dist/features/catalogue/actions/list-from-catalogue.js +2 -2
  52. package/dist/features/catalogue/actions/submit-for-approval.js +1 -4
  53. package/dist/features/catalogue/components/AdminCatalogueApprovalsView.js +5 -1
  54. package/dist/features/catalogue/repository/catalogue.repository.d.ts +23 -0
  55. package/dist/features/catalogue/repository/catalogue.repository.js +28 -1
  56. package/dist/features/catalogue/schemas/firestore.d.ts +19 -0
  57. package/dist/features/catalogue/schemas/firestore.js +12 -0
  58. package/dist/features/categories/schemas/index.d.ts +2 -2
  59. package/dist/features/email/index.d.ts +1 -1
  60. package/dist/features/email/index.js +5 -1
  61. package/dist/features/email/notification-templates.d.ts +35 -0
  62. package/dist/features/email/notification-templates.js +96 -0
  63. package/dist/features/messages/schemas/index.d.ts +6 -6
  64. package/dist/features/orders/schemas/index.d.ts +14 -14
  65. package/dist/features/payments/repository/payout.repository.d.ts +18 -5
  66. package/dist/features/payments/repository/payout.repository.js +45 -19
  67. package/dist/features/payments/schemas/firestore.d.ts +33 -1
  68. package/dist/features/payments/schemas/firestore.js +39 -0
  69. package/dist/features/payments/schemas/index.d.ts +12 -12
  70. package/dist/features/products/schemas/index.d.ts +2 -2
  71. package/dist/features/promotions/schemas/index.d.ts +12 -12
  72. package/dist/features/scams/repository/scammer.repository.d.ts +18 -1
  73. package/dist/features/scams/repository/scammer.repository.js +24 -2
  74. package/dist/features/scams/schemas/firestore.d.ts +24 -0
  75. package/dist/features/scams/schemas/firestore.js +30 -0
  76. package/dist/features/shipments/components/AdminShipmentEditorView.js +2 -1
  77. package/dist/features/shipments/repository/shipments.repository.d.ts +18 -0
  78. package/dist/features/shipments/repository/shipments.repository.js +23 -1
  79. package/dist/features/shipments/schemas/firestore.d.ts +14 -0
  80. package/dist/features/shipments/schemas/firestore.js +19 -0
  81. package/dist/features/status-history/components/RecordStatusTimeline.d.ts +12 -0
  82. package/dist/features/status-history/components/RecordStatusTimeline.js +90 -0
  83. package/dist/features/status-history/index.d.ts +2 -0
  84. package/dist/features/status-history/index.js +1 -0
  85. package/dist/features/stores/repository/store.repository.d.ts +30 -2
  86. package/dist/features/stores/repository/store.repository.js +43 -4
  87. package/dist/features/stores/schemas/firestore.d.ts +29 -0
  88. package/dist/features/stores/schemas/firestore.js +31 -0
  89. package/dist/features/support/repository/support.repository.d.ts +47 -3
  90. package/dist/features/support/repository/support.repository.js +72 -12
  91. package/dist/features/support/schemas/firestore.d.ts +38 -1
  92. package/dist/features/support/schemas/firestore.js +34 -0
  93. package/dist/features/support/schemas/index.d.ts +2 -2
  94. package/dist/features/tester/seed-data/tester-checklist-seed-data.js +84 -0
  95. package/dist/index.d.ts +7 -4
  96. package/dist/index.js +15 -2
  97. package/dist/schemas/registry.d.ts +34 -34
  98. package/dist/seed/bids-seed-data.js +8 -0
  99. package/dist/seed/factories/notification.factory.d.ts +2 -1
  100. package/dist/server-entry.d.ts +1 -0
  101. package/dist/server-entry.js +5 -0
  102. package/dist/server.d.ts +5 -1
  103. package/dist/server.js +10 -1
  104. package/dist/styles.css +58 -1
  105. package/dist/tailwind-utilities.css +1 -1
  106. package/dist/ui/components/QuantityStepper.d.ts +38 -0
  107. package/dist/ui/components/QuantityStepper.js +20 -0
  108. package/dist/ui/components/QuantityStepper.style.css +56 -0
  109. package/dist/ui/components/index.style.css +1 -0
  110. package/dist/ui/index.d.ts +2 -0
  111. package/dist/ui/index.js +1 -0
  112. package/package.json +1 -1
@@ -3,9 +3,32 @@
3
3
  */
4
4
  import { BaseRepository, parseSieveDateValue, type SieveModel, type FirebaseSieveResult } from "../../../providers/db-firebase";
5
5
  import { type CatalogueItemDocument, type CatalogueItemCreateInput, type CatalogueItemUpdateInput } from "../schemas/firestore";
6
+ import { type HistoryActor } from "../../../_internal/shared/history/index";
7
+ /** Who/why for a catalogue write that lands in `statusHistory`. */
8
+ export interface CatalogueWriteContext {
9
+ actor?: HistoryActor;
10
+ trigger?: string;
11
+ reason?: string;
12
+ note?: string;
13
+ }
6
14
  export declare class CatalogueRepository extends BaseRepository<CatalogueItemDocument> {
7
15
  constructor();
8
16
  create(input: CatalogueItemCreateInput): Promise<CatalogueItemDocument>;
17
+ /**
18
+ * The approval-workflow write path — the funnel this repository did not have.
19
+ *
20
+ * `listingStatus` was moved by FIVE scattered `update()` calls across four
21
+ * action files and a scheduled job, none of which recorded who did it or
22
+ * why. `rejectionReason` was the only trace an admin decision left, and the
23
+ * next decision overwrites it.
24
+ *
25
+ * Every caller already holds the item (each does its own `findById` to
26
+ * authorise the transition), so `prior` makes the timeline entry free.
27
+ * Deliberately NOT a `BaseRepository` hook: `update()` does not read before
28
+ * writing, so a generic hook would force a pre-read on every update of
29
+ * every collection (Rule #6), and it carries neither actor nor trigger.
30
+ */
31
+ setListingStatus(itemId: string, patch: CatalogueItemUpdateInput, ctx?: CatalogueWriteContext, prior?: CatalogueItemDocument | null): Promise<CatalogueItemDocument>;
9
32
  /** Stamps lastImageUpdateAt whenever `images` changes — the single field the freshness gate checks. */
10
33
  update(itemId: string, input: CatalogueItemUpdateInput): Promise<CatalogueItemDocument>;
11
34
  listPublicByOwner(ownerId: string): Promise<CatalogueItemDocument[]>;
@@ -2,7 +2,8 @@
2
2
  * Personal Catalogue Repository (Feature B).
3
3
  */
4
4
  import { BaseRepository, prepareForFirestore, parseSieveDateValue, } from "../../../providers/db-firebase";
5
- import { CATALOGUE_COLLECTION, CATALOGUE_FIELDS, createCatalogueItemId, } from "../schemas/firestore";
5
+ import { CATALOGUE_COLLECTION, CATALOGUE_FIELDS, CATALOGUE_TRACKED_FIELDS, CATALOGUE_HISTORY_PII_FIELDS, createCatalogueItemId, } from "../schemas/firestore";
6
+ import { withHistory } from "../../../_internal/shared/history/index";
6
7
  export class CatalogueRepository extends BaseRepository {
7
8
  constructor() {
8
9
  super(CATALOGUE_COLLECTION);
@@ -20,6 +21,32 @@ export class CatalogueRepository extends BaseRepository {
20
21
  await this.getCollection().doc(id).set(prepareForFirestore(data));
21
22
  return { id, ...data };
22
23
  }
24
+ /**
25
+ * The approval-workflow write path — the funnel this repository did not have.
26
+ *
27
+ * `listingStatus` was moved by FIVE scattered `update()` calls across four
28
+ * action files and a scheduled job, none of which recorded who did it or
29
+ * why. `rejectionReason` was the only trace an admin decision left, and the
30
+ * next decision overwrites it.
31
+ *
32
+ * Every caller already holds the item (each does its own `findById` to
33
+ * authorise the transition), so `prior` makes the timeline entry free.
34
+ * Deliberately NOT a `BaseRepository` hook: `update()` does not read before
35
+ * writing, so a generic hook would force a pre-read on every update of
36
+ * every collection (Rule #6), and it carries neither actor nor trigger.
37
+ */
38
+ async setListingStatus(itemId, patch, ctx, prior) {
39
+ const current = prior !== undefined ? prior : await this.findById(itemId);
40
+ const withEntry = withHistory(current, patch, {
41
+ tracked: CATALOGUE_TRACKED_FIELDS,
42
+ actor: ctx?.actor ?? { role: "system" },
43
+ trigger: ctx?.trigger ?? "setListingStatus",
44
+ reason: ctx?.reason,
45
+ note: ctx?.note,
46
+ piiFields: CATALOGUE_HISTORY_PII_FIELDS,
47
+ });
48
+ return this.update(itemId, withEntry ?? patch);
49
+ }
23
50
  /** Stamps lastImageUpdateAt whenever `images` changes — the single field the freshness gate checks. */
24
51
  async update(itemId, input) {
25
52
  const patch = { ...input };
@@ -7,6 +7,7 @@
7
7
  * `mycatalog-` to stay visually distinct from `catalog-`.
8
8
  */
9
9
  import { type GenerateCatalogueItemIdInput } from "../../../utils/id-generators";
10
+ import type { StatusChangeEntry } from "../../../_internal/shared/history/types";
10
11
  import type { BaseDocument } from "../../../_internal/shared/types/base-document";
11
12
  import type { ProductDraftFields } from "../../shipments/schemas/firestore";
12
13
  export declare const CATALOGUE_COLLECTION: "catalogueItems";
@@ -32,6 +33,17 @@ export interface CatalogueItemDocument extends BaseDocument, Partial<Omit<Produc
32
33
  rejectionReason?: string;
33
34
  linkedProductId?: string;
34
35
  linkedProductSlug?: string;
36
+ /**
37
+ * Who changed what, when, and why. See § "Status History" in CLAUDE.md.
38
+ *
39
+ * A catalogue item's status is a small approval workflow between an owner
40
+ * and an admin (`not_listed → pending_admin_approval → listed | rejected`),
41
+ * so "why was mine rejected and by whom" is exactly the question the
42
+ * timeline exists for. `rejectionReason` alone is overwritten by the next
43
+ * decision.
44
+ */
45
+ statusHistory?: StatusChangeEntry[];
46
+ statusHistoryTruncated?: number;
35
47
  }
36
48
  export declare const CATALOGUE_IMAGE_FRESHNESS_DAYS: 30;
37
49
  export declare const CATALOGUE_FIELDS: {
@@ -45,3 +57,10 @@ export declare const CATALOGUE_FIELDS: {
45
57
  export type CatalogueItemCreateInput = Omit<CatalogueItemDocument, "id" | "createdAt" | "updatedAt" | "lastImageUpdateAt" | "listingStatus">;
46
58
  export type CatalogueItemUpdateInput = Partial<Omit<CatalogueItemDocument, "id" | "createdAt" | "ownerId">>;
47
59
  export declare function createCatalogueItemId(input: GenerateCatalogueItemIdInput): string;
60
+ /** The fields whose changes earn a timeline entry. */
61
+ export declare const CATALOGUE_TRACKED_FIELDS: readonly ["listingStatus", "rejectionReason", "linkedProductId", "submittedForApprovalAt"];
62
+ /**
63
+ * PII on this document, for `withHistory`'s scrub. `ownerId` is a UID, not
64
+ * PII, and is what the timeline's actor already carries.
65
+ */
66
+ export declare const CATALOGUE_HISTORY_PII_FIELDS: readonly ["ownerName", "ownerEmail"];
@@ -20,3 +20,15 @@ export const CATALOGUE_FIELDS = {
20
20
  export function createCatalogueItemId(input) {
21
21
  return generateCatalogueItemId(input);
22
22
  }
23
+ /** The fields whose changes earn a timeline entry. */
24
+ export const CATALOGUE_TRACKED_FIELDS = [
25
+ "listingStatus",
26
+ "rejectionReason",
27
+ "linkedProductId",
28
+ "submittedForApprovalAt",
29
+ ];
30
+ /**
31
+ * PII on this document, for `withHistory`'s scrub. `ownerId` is a UID, not
32
+ * PII, and is what the timeline's actor already carries.
33
+ */
34
+ export const CATALOGUE_HISTORY_PII_FIELDS = ["ownerName", "ownerEmail"];
@@ -452,7 +452,7 @@ export declare const categoryFirestoreSchema: z.ZodObject<{
452
452
  }, z.ZodTypeAny, "passthrough"> | undefined;
453
453
  featuredPriority?: number | undefined;
454
454
  isBrand?: boolean | undefined;
455
- createdByType?: "store" | "admin" | undefined;
455
+ createdByType?: "admin" | "store" | undefined;
456
456
  createdByStoreId?: string | undefined;
457
457
  itemCode?: string | undefined;
458
458
  brandWebsite?: string | undefined;
@@ -551,7 +551,7 @@ export declare const categoryFirestoreSchema: z.ZodObject<{
551
551
  }, z.ZodTypeAny, "passthrough"> | undefined;
552
552
  featuredPriority?: number | undefined;
553
553
  isBrand?: boolean | undefined;
554
- createdByType?: "store" | "admin" | undefined;
554
+ createdByType?: "admin" | "store" | undefined;
555
555
  createdByStoreId?: string | undefined;
556
556
  itemCode?: string | undefined;
557
557
  brandWebsite?: string | undefined;
@@ -5,5 +5,5 @@
5
5
  * `renderToStaticMarkup`. They emit table-based, inline-styled markup that
6
6
  * email clients render correctly.
7
7
  */
8
- export { EmailButton, EmailColumn, EmailContainer, EmailDivider, EmailDoc, EmailFooter, EmailHeader, EmailImage, EmailLink, EmailRow, } from "./primitives";
8
+ export { EmailBold, EmailButton, EmailColumn, EmailContainer, EmailDivider, EmailDoc, EmailFooter, EmailHeader, EmailImage, EmailLink, EmailRow, } from "./primitives";
9
9
  export type { EmailButtonProps, EmailColumnProps, EmailContainerProps, EmailDividerProps, EmailDocProps, EmailFooterProps, EmailHeaderProps, EmailImageProps, EmailLinkProps, EmailRowProps, EmailTone, } from "./primitives";
@@ -5,4 +5,8 @@
5
5
  * `renderToStaticMarkup`. They emit table-based, inline-styled markup that
6
6
  * email clients render correctly.
7
7
  */
8
- export { EmailButton, EmailColumn, EmailContainer, EmailDivider, EmailDoc, EmailFooter, EmailHeader, EmailImage, EmailLink, EmailRow, } from "./primitives";
8
+ export {
9
+ // Was MISSING from this barrel, so both consumers reached past it into
10
+ // ./primitives directly — a workaround that reads as intentional and
11
+ // quietly says "the barrel is not the public surface after all".
12
+ EmailBold, EmailButton, EmailColumn, EmailContainer, EmailDivider, EmailDoc, EmailFooter, EmailHeader, EmailImage, EmailLink, EmailRow, } from "./primitives";
@@ -0,0 +1,35 @@
1
+ import "server-only";
2
+ import type { NotificationType } from "../admin/schemas/firestore";
3
+ export interface NotificationEmailContext {
4
+ title: string;
5
+ message: string;
6
+ siteName: string;
7
+ /** Absolute URL the CTA opens. Absent → the email renders without a button. */
8
+ actionUrl?: string;
9
+ }
10
+ export interface NotificationTemplateDef {
11
+ /** CTA label. Absent → no button, for a type with nowhere to go. */
12
+ cta?: string;
13
+ /**
14
+ * One line of framing above the message, so the reader knows what KIND of
15
+ * mail this is before reading it. Deliberately not the message itself —
16
+ * duplicating it is worse than omitting it.
17
+ */
18
+ lead: string;
19
+ /** Terminal-negative types render in the danger tone. */
20
+ tone?: "default" | "danger" | "success";
21
+ }
22
+ /**
23
+ * 🛑 Every notification type, no exceptions.
24
+ *
25
+ * When adding a type, the compiler will ask for an entry here. Answer three
26
+ * questions: what does the reader need to know before the message (`lead`),
27
+ * where are they going (`cta`, or none if nowhere), and is this good news.
28
+ */
29
+ export declare const NOTIFICATION_EMAIL_TEMPLATES: Record<NotificationType, NotificationTemplateDef>;
30
+ /**
31
+ * Render one notification as an email.
32
+ *
33
+ * Returns the full HTML document, ready for `emailHtml`.
34
+ */
35
+ export declare function renderNotificationEmail(type: NotificationType, ctx: NotificationEmailContext): string;
@@ -0,0 +1,96 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import "server-only";
3
+ import { EmailBold, EmailButton, EmailContainer, EmailDivider, EmailDoc, EmailFooter, EmailHeader, EmailRow, } from "./primitives";
4
+ /**
5
+ * A real email per notification type.
6
+ *
7
+ * ## What this replaces
8
+ *
9
+ * `emailHtml` has existed on `SendNotificationInput` since the channel was
10
+ * built, and **0 of 40** call sites passed it — so every one of the 28
11
+ * notification types fell through to `html: \`<p>${message}</p>\``. A bare
12
+ * paragraph, no subject context, no branding, nothing to click. That is the
13
+ * reported "emails are sent but I can't understand what is being sent".
14
+ *
15
+ * ## Why a Record, not a lookup with a fallback
16
+ *
17
+ * `Record<NotificationType, TemplateDef>` — a new notification type cannot
18
+ * compile without an email. A `Partial` with a default would have let the
19
+ * next type silently go back to `<p>{message}</p>`, which is how the feature
20
+ * got here in the first place.
21
+ *
22
+ * ## The two structural constraints
23
+ *
24
+ * 1. **`renderToStaticMarkup` must be a lazy `require`.** A top-level
25
+ * `import` from "react-dom/server" is flagged by Next's static tracer as
26
+ * soon as any module in the re-export chain is reachable from a Server
27
+ * Component — and this one is, via `@mohasinac/appkit/server`. `require()`
28
+ * is opaque to that analyser, so the chain stops here. Same reason
29
+ * `features/contact/email.tsx` does it.
30
+ * 2. **Never a raw `<table>`.** The `Email*` primitives emit the table-based,
31
+ * inline-styled markup email clients actually render; hand-rolled markup
32
+ * is what `audit-email-raw-html` blocks.
33
+ */
34
+ // See constraint (1) above — this MUST stay a lazy require.
35
+ function renderToStaticMarkup(el) {
36
+ const m = require("react-dom/server");
37
+ return m.renderToStaticMarkup(el);
38
+ }
39
+ /**
40
+ * 🛑 Every notification type, no exceptions.
41
+ *
42
+ * When adding a type, the compiler will ask for an entry here. Answer three
43
+ * questions: what does the reader need to know before the message (`lead`),
44
+ * where are they going (`cta`, or none if nowhere), and is this good news.
45
+ */
46
+ export const NOTIFICATION_EMAIL_TEMPLATES = {
47
+ order_placed: { lead: "We've received your order.", cta: "View order", tone: "success" },
48
+ order_confirmed: { lead: "Your order is confirmed and being prepared.", cta: "View order", tone: "success" },
49
+ order_shipped: { lead: "Your order is on its way.", cta: "Track order", tone: "success" },
50
+ order_delivered: { lead: "Your order has been delivered.", cta: "View order", tone: "success" },
51
+ order_cancelled: { lead: "Your order has been cancelled.", cta: "View order", tone: "danger" },
52
+ refund_initiated: { lead: "A refund is on its way back to you.", cta: "View order" },
53
+ payment_review: { lead: "There's an update on your payment.", cta: "View payment" },
54
+ emi_installment_due_soon: { lead: "An EMI installment is due soon.", cta: "View order" },
55
+ emi_installment_overdue: { lead: "An EMI installment is overdue.", cta: "View order", tone: "danger" },
56
+ bid_placed: { lead: "Your bid has been placed.", cta: "View bids" },
57
+ bid_outbid: { lead: "Someone has outbid you.", cta: "View bids", tone: "danger" },
58
+ bid_won: { lead: "You won the auction.", cta: "Complete checkout", tone: "success" },
59
+ bid_lost: { lead: "This auction closed without you.", cta: "View bids" },
60
+ auction_ended: { lead: "Your auction has ended.", cta: "View auction" },
61
+ offer_received: { lead: "A buyer has made you an offer.", cta: "Review offer" },
62
+ offer_responded: { lead: "The seller has responded to your offer.", cta: "View offer" },
63
+ offer_counter_accepted: { lead: "Your counter-offer was accepted.", cta: "Complete checkout", tone: "success" },
64
+ offer_expired: { lead: "An offer has expired.", cta: "View offers" },
65
+ prize_won: { lead: "You've won a prize.", cta: "View prize", tone: "success" },
66
+ prize_reveal_expired: { lead: "A prize reveal window has closed.", cta: "View prize draws" },
67
+ review_approved: { lead: "A review has been published.", cta: "Read review" },
68
+ review_replied: { lead: "Someone replied to your review.", cta: "Read reply" },
69
+ product_available: { lead: "Something you were watching is back in stock.", cta: "View listing" },
70
+ catalogue_images_stale: { lead: "Your catalogue photos need refreshing.", cta: "Update photos" },
71
+ welcome: { lead: "Welcome aboard.", cta: "Go to your account", tone: "success" },
72
+ account_action: { lead: "There's been a change to your account.", cta: "View account" },
73
+ // No CTA: a promotion's destination is whatever the copy names, and a
74
+ // generic "View" button on a marketing mail is worse than none.
75
+ promotion: { lead: "An offer you might like." },
76
+ // No CTA for the same reason — a system announcement has no record behind
77
+ // it, and `resolveNotificationActionUrl` correctly returns undefined.
78
+ system: { lead: "A message from the team." },
79
+ };
80
+ /**
81
+ * Render one notification as an email.
82
+ *
83
+ * Returns the full HTML document, ready for `emailHtml`.
84
+ */
85
+ export function renderNotificationEmail(type, ctx) {
86
+ const def = NOTIFICATION_EMAIL_TEMPLATES[type];
87
+ /*
88
+ * `EmailButton` takes a narrower tone set than `EmailTone` — brand / accent
89
+ * / neutral only, because a button is a call to action and there is no
90
+ * useful "danger" button. So the template's tone drives the BUTTON's
91
+ * emphasis, not its colour: bad news gets the neutral button ("View order"
92
+ * on a cancellation should not shout), good news gets the brand one.
93
+ */
94
+ const buttonTone = def.tone === "danger" ? "neutral" : "brand";
95
+ return `<!DOCTYPE html>${renderToStaticMarkup(_jsx(EmailDoc, { title: ctx.title, children: _jsxs(EmailContainer, { children: [_jsx(EmailHeader, { brandName: ctx.siteName }), _jsxs(EmailRow, { children: [_jsx(EmailBold, { children: ctx.title }), _jsx("br", {}), _jsx("br", {}), def.lead] }), _jsx(EmailDivider, {}), _jsx(EmailRow, { children: ctx.message }), def.cta && ctx.actionUrl ? (_jsx(EmailRow, { children: _jsx(EmailButton, { href: ctx.actionUrl, tone: buttonTone, children: def.cta }) })) : null, _jsx(EmailFooter, { copyright: `© ${ctx.siteName}` })] }) }))}`;
96
+ }
@@ -21,7 +21,7 @@ export declare const conversationMessageSchema: z.ZodObject<{
21
21
  body: string;
22
22
  isRead: boolean;
23
23
  senderId: string;
24
- senderRole: "seller" | "buyer";
24
+ senderRole: "buyer" | "seller";
25
25
  sentAt: string | Date | z.objectOutputType<{
26
26
  toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
27
27
  }, z.ZodTypeAny, "passthrough">;
@@ -31,7 +31,7 @@ export declare const conversationMessageSchema: z.ZodObject<{
31
31
  body: string;
32
32
  isRead: boolean;
33
33
  senderId: string;
34
- senderRole: "seller" | "buyer";
34
+ senderRole: "buyer" | "seller";
35
35
  sentAt: string | Date | z.objectInputType<{
36
36
  toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
37
37
  }, z.ZodTypeAny, "passthrough">;
@@ -79,7 +79,7 @@ export declare const conversationFirestoreSchema: z.ZodObject<{
79
79
  body: string;
80
80
  isRead: boolean;
81
81
  senderId: string;
82
- senderRole: "seller" | "buyer";
82
+ senderRole: "buyer" | "seller";
83
83
  sentAt: string | Date | z.objectOutputType<{
84
84
  toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
85
85
  }, z.ZodTypeAny, "passthrough">;
@@ -89,7 +89,7 @@ export declare const conversationFirestoreSchema: z.ZodObject<{
89
89
  body: string;
90
90
  isRead: boolean;
91
91
  senderId: string;
92
- senderRole: "seller" | "buyer";
92
+ senderRole: "buyer" | "seller";
93
93
  sentAt: string | Date | z.objectInputType<{
94
94
  toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
95
95
  }, z.ZodTypeAny, "passthrough">;
@@ -127,7 +127,7 @@ export declare const conversationFirestoreSchema: z.ZodObject<{
127
127
  body: string;
128
128
  isRead: boolean;
129
129
  senderId: string;
130
- senderRole: "seller" | "buyer";
130
+ senderRole: "buyer" | "seller";
131
131
  sentAt: string | Date | z.objectOutputType<{
132
132
  toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
133
133
  }, z.ZodTypeAny, "passthrough">;
@@ -160,7 +160,7 @@ export declare const conversationFirestoreSchema: z.ZodObject<{
160
160
  body: string;
161
161
  isRead: boolean;
162
162
  senderId: string;
163
- senderRole: "seller" | "buyer";
163
+ senderRole: "buyer" | "seller";
164
164
  sentAt: string | Date | z.objectInputType<{
165
165
  toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
166
166
  }, z.ZodTypeAny, "passthrough">;
@@ -128,9 +128,9 @@ export declare const orderRefundEventSchema: z.ZodObject<{
128
128
  overrideReason: z.ZodOptional<z.ZodString>;
129
129
  }, "strip", z.ZodTypeAny, {
130
130
  type: "full" | "partial";
131
+ reason: string;
131
132
  amount: number;
132
133
  refundId: string;
133
- reason: string;
134
134
  refundedAt: string | Date | z.objectOutputType<{
135
135
  toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
136
136
  }, z.ZodTypeAny, "passthrough">;
@@ -143,9 +143,9 @@ export declare const orderRefundEventSchema: z.ZodObject<{
143
143
  overrideReason?: string | undefined;
144
144
  }, {
145
145
  type: "full" | "partial";
146
+ reason: string;
146
147
  amount: number;
147
148
  refundId: string;
148
- reason: string;
149
149
  refundedAt: string | Date | z.objectInputType<{
150
150
  toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
151
151
  }, z.ZodTypeAny, "passthrough">;
@@ -168,14 +168,14 @@ export declare const appliedOrderDiscountSchema: z.ZodObject<{
168
168
  code: string;
169
169
  type: "auto" | "coupon" | "deal";
170
170
  discountAmount: number;
171
- scope?: "admin" | "seller" | undefined;
171
+ scope?: "seller" | "admin" | undefined;
172
172
  storeId?: string | undefined;
173
173
  couponId?: string | undefined;
174
174
  }, {
175
175
  code: string;
176
176
  type: "auto" | "coupon" | "deal";
177
177
  discountAmount: number;
178
- scope?: "admin" | "seller" | undefined;
178
+ scope?: "seller" | "admin" | undefined;
179
179
  storeId?: string | undefined;
180
180
  couponId?: string | undefined;
181
181
  }>;
@@ -326,14 +326,14 @@ export declare const orderFirestoreSchema: z.ZodObject<{
326
326
  code: string;
327
327
  type: "auto" | "coupon" | "deal";
328
328
  discountAmount: number;
329
- scope?: "admin" | "seller" | undefined;
329
+ scope?: "seller" | "admin" | undefined;
330
330
  storeId?: string | undefined;
331
331
  couponId?: string | undefined;
332
332
  }, {
333
333
  code: string;
334
334
  type: "auto" | "coupon" | "deal";
335
335
  discountAmount: number;
336
- scope?: "admin" | "seller" | undefined;
336
+ scope?: "seller" | "admin" | undefined;
337
337
  storeId?: string | undefined;
338
338
  couponId?: string | undefined;
339
339
  }>, "many">>;
@@ -464,9 +464,9 @@ export declare const orderFirestoreSchema: z.ZodObject<{
464
464
  overrideReason: z.ZodOptional<z.ZodString>;
465
465
  }, "strip", z.ZodTypeAny, {
466
466
  type: "full" | "partial";
467
+ reason: string;
467
468
  amount: number;
468
469
  refundId: string;
469
- reason: string;
470
470
  refundedAt: string | Date | z.objectOutputType<{
471
471
  toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
472
472
  }, z.ZodTypeAny, "passthrough">;
@@ -479,9 +479,9 @@ export declare const orderFirestoreSchema: z.ZodObject<{
479
479
  overrideReason?: string | undefined;
480
480
  }, {
481
481
  type: "full" | "partial";
482
+ reason: string;
482
483
  amount: number;
483
484
  refundId: string;
484
- reason: string;
485
485
  refundedAt: string | Date | z.objectInputType<{
486
486
  toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
487
487
  }, z.ZodTypeAny, "passthrough">;
@@ -612,6 +612,7 @@ export declare const orderFirestoreSchema: z.ZodObject<{
612
612
  shelf: string;
613
613
  bin: string;
614
614
  } | undefined;
615
+ offerId?: string | undefined;
615
616
  orderType?: string | undefined;
616
617
  imageUrls?: string[] | undefined;
617
618
  refundFeeDeducted?: number | undefined;
@@ -622,7 +623,7 @@ export declare const orderFirestoreSchema: z.ZodObject<{
622
623
  code: string;
623
624
  type: "auto" | "coupon" | "deal";
624
625
  discountAmount: number;
625
- scope?: "admin" | "seller" | undefined;
626
+ scope?: "seller" | "admin" | undefined;
626
627
  storeId?: string | undefined;
627
628
  couponId?: string | undefined;
628
629
  }[] | undefined;
@@ -649,14 +650,13 @@ export declare const orderFirestoreSchema: z.ZodObject<{
649
650
  }[] | undefined;
650
651
  emiRemainingBalance?: number | undefined;
651
652
  emiComplete?: boolean | undefined;
652
- offerId?: string | undefined;
653
653
  isNonRefundable?: boolean | undefined;
654
654
  paymentBatchId?: string | undefined;
655
655
  refunds?: {
656
656
  type: "full" | "partial";
657
+ reason: string;
657
658
  amount: number;
658
659
  refundId: string;
659
- reason: string;
660
660
  refundedAt: string | Date | z.objectOutputType<{
661
661
  toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
662
662
  }, z.ZodTypeAny, "passthrough">;
@@ -760,6 +760,7 @@ export declare const orderFirestoreSchema: z.ZodObject<{
760
760
  shelf: string;
761
761
  bin: string;
762
762
  } | undefined;
763
+ offerId?: string | undefined;
763
764
  orderType?: string | undefined;
764
765
  imageUrls?: string[] | undefined;
765
766
  refundFeeDeducted?: number | undefined;
@@ -770,7 +771,7 @@ export declare const orderFirestoreSchema: z.ZodObject<{
770
771
  code: string;
771
772
  type: "auto" | "coupon" | "deal";
772
773
  discountAmount: number;
773
- scope?: "admin" | "seller" | undefined;
774
+ scope?: "seller" | "admin" | undefined;
774
775
  storeId?: string | undefined;
775
776
  couponId?: string | undefined;
776
777
  }[] | undefined;
@@ -797,14 +798,13 @@ export declare const orderFirestoreSchema: z.ZodObject<{
797
798
  }[] | undefined;
798
799
  emiRemainingBalance?: number | undefined;
799
800
  emiComplete?: boolean | undefined;
800
- offerId?: string | undefined;
801
801
  isNonRefundable?: boolean | undefined;
802
802
  paymentBatchId?: string | undefined;
803
803
  refunds?: {
804
804
  type: "full" | "partial";
805
+ reason: string;
805
806
  amount: number;
806
807
  refundId: string;
807
- reason: string;
808
808
  refundedAt: string | Date | z.objectInputType<{
809
809
  toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
810
810
  }, z.ZodTypeAny, "passthrough">;
@@ -7,6 +7,14 @@
7
7
  import { BaseRepository, parseSieveDateValue, type SieveModel, type FirebaseSieveResult } from "../../../providers/db-firebase";
8
8
  import type { DocumentReference, WriteResult } from "firebase-admin/firestore";
9
9
  import type { PayoutDocument, PayoutCreateInput, PayoutRefundDeduction, PayoutStatus, PayoutUpdateInput } from "../schemas";
10
+ import { type HistoryActor } from "../../../_internal/shared/history/index";
11
+ /** Who/why for a payout write that lands in `statusHistory`. */
12
+ export interface PayoutWriteContext {
13
+ actor?: HistoryActor;
14
+ trigger?: string;
15
+ reason?: string;
16
+ note?: string;
17
+ }
10
18
  export declare class PayoutRepository extends BaseRepository<PayoutDocument> {
11
19
  constructor();
12
20
  /** Decrypt PII fields on a payout document after Firestore read */
@@ -46,12 +54,17 @@ export declare class PayoutRepository extends BaseRepository<PayoutDocument> {
46
54
  data: PayoutDocument;
47
55
  }>>;
48
56
  /**
49
- * Update payout status (admin action)
57
+ * Update payout status (admin action).
58
+ *
59
+ * `prior` keeps the timeline entry read-free — the admin route already
60
+ * fetches the payout to authorise the change (Rule #6).
50
61
  */
51
- updateStatus(payoutId: string, status: PayoutStatus, extra?: PayoutUpdateInput): Promise<PayoutDocument>;
52
- markProcessing(ref: DocumentReference): Promise<WriteResult>;
53
- recordSuccess(ref: DocumentReference, razorpayPayoutId: string, razorpayStatus: string): Promise<WriteResult>;
54
- recordFailure(ref: DocumentReference, failureCount: number, reason: string, isFinal: boolean): Promise<WriteResult>;
62
+ updateStatus(payoutId: string, status: PayoutStatus, extra?: PayoutUpdateInput, ctx?: PayoutWriteContext, prior?: PayoutDocument | null): Promise<PayoutDocument>;
63
+ markProcessing(ref: DocumentReference, ctx?: PayoutWriteContext, prior?: PayoutDocument | null): Promise<WriteResult>;
64
+ recordSuccess(ref: DocumentReference, razorpayPayoutId: string, razorpayStatus: string, ctx?: PayoutWriteContext, prior?: PayoutDocument | null): Promise<WriteResult>;
65
+ recordFailure(ref: DocumentReference, failureCount: number, reason: string, isFinal: boolean, ctx?: PayoutWriteContext, prior?: PayoutDocument | null): Promise<WriteResult>;
66
+ /** Append a timeline entry when the patch touches a tracked field. */
67
+ private withPayoutHistory;
55
68
  /**
56
69
  * Get all order IDs that have already been paid out for a seller.
57
70
  * Used to avoid double-paying the same orders.
@@ -5,7 +5,8 @@
5
5
  * Handles seller payout requests and admin payout processing.
6
6
  */
7
7
  import { BaseRepository, prepareForFirestore, parseSieveDateValue, } from "../../../providers/db-firebase";
8
- import { PAYOUT_COLLECTION, PAYOUT_FIELDS, createPayoutId } from "../schemas";
8
+ import { PAYOUT_COLLECTION, PAYOUT_FIELDS, PAYOUT_TRACKED_FIELDS, createPayoutId } from "../schemas";
9
+ import { withHistory } from "../../../_internal/shared/history/index";
9
10
  import { encryptPiiFields, decryptPiiFields, addPiiIndices, PAYOUT_PII_FIELDS, PAYOUT_PII_INDEX_MAP, encryptPayoutBankAccount, decryptPayoutBankAccount, } from "../../../security";
10
11
  export class PayoutRepository extends BaseRepository {
11
12
  constructor() {
@@ -120,10 +121,14 @@ export class PayoutRepository extends BaseRepository {
120
121
  }));
121
122
  }
122
123
  /**
123
- * Update payout status (admin action)
124
+ * Update payout status (admin action).
125
+ *
126
+ * `prior` keeps the timeline entry read-free — the admin route already
127
+ * fetches the payout to authorise the change (Rule #6).
124
128
  */
125
- async updateStatus(payoutId, status, extra) {
126
- return this.update(payoutId, {
129
+ async updateStatus(payoutId, status, extra, ctx, prior) {
130
+ const current = prior !== undefined ? prior : await this.findById(payoutId);
131
+ return this.update(payoutId, this.withPayoutHistory(current, {
127
132
  status,
128
133
  ...extra,
129
134
  ...(status === PAYOUT_FIELDS.STATUS_VALUES.PAID ||
@@ -131,29 +136,50 @@ export class PayoutRepository extends BaseRepository {
131
136
  ? { processedAt: new Date() }
132
137
  : {}),
133
138
  updatedAt: new Date(),
134
- });
139
+ }, ctx, "adminUpdatePayout"));
135
140
  }
136
- markProcessing(ref) {
137
- return ref.update({
138
- status: "processing",
139
- processedAt: new Date(),
140
- updatedAt: new Date(),
141
- });
141
+ /*
142
+ * ── Batch dispatch primitives ──────────────────────────────────────────
143
+ *
144
+ * These take a `DocumentReference` rather than an id because the payouts
145
+ * batch already holds one from its query snapshot. `prior` comes from the
146
+ * SAME snapshot, so history costs no read here either — but it must be the
147
+ * caller's running copy, not the original: the sequence is
148
+ * pending → processing → failed, and diffing the final write against the
149
+ * ORIGINAL snapshot would record "pending → failed" and lose the dispatch
150
+ * attempt entirely.
151
+ */
152
+ markProcessing(ref, ctx, prior) {
153
+ return ref.update(this.withPayoutHistory(prior, { status: "processing", processedAt: new Date(), updatedAt: new Date() }, ctx, "payoutBatch:dispatch"));
142
154
  }
143
- recordSuccess(ref, razorpayPayoutId, razorpayStatus) {
144
- return ref.update({
145
- razorpayPayoutId,
146
- razorpayStatus,
147
- updatedAt: new Date(),
148
- });
155
+ recordSuccess(ref, razorpayPayoutId, razorpayStatus, ctx, prior) {
156
+ return ref.update(this.withPayoutHistory(prior, { razorpayPayoutId, razorpayStatus, updatedAt: new Date() }, ctx, "payoutBatch:accepted"));
149
157
  }
150
- recordFailure(ref, failureCount, reason, isFinal) {
151
- return ref.update({
158
+ recordFailure(ref, failureCount, reason, isFinal, ctx, prior) {
159
+ return ref.update(this.withPayoutHistory(prior, {
152
160
  status: isFinal ? "failed" : "pending",
153
161
  failureCount,
154
162
  lastFailureReason: reason,
155
163
  updatedAt: new Date(),
164
+ },
165
+ // The reason is the point of the entry — `lastFailureReason` is
166
+ // overwritten by the next retry, so history is where the earlier
167
+ // attempts survive.
168
+ { ...ctx, reason }, isFinal ? "payoutBatch:failedFinal" : "payoutBatch:failedRetrying"));
169
+ }
170
+ /** Append a timeline entry when the patch touches a tracked field. */
171
+ withPayoutHistory(current, patch, ctx, defaultTrigger) {
172
+ const withEntry = withHistory(current, patch, {
173
+ tracked: PAYOUT_TRACKED_FIELDS,
174
+ actor: ctx?.actor ?? { role: "system" },
175
+ trigger: ctx?.trigger ?? defaultTrigger,
176
+ reason: ctx?.reason,
177
+ note: ctx?.note,
178
+ // `sellerEmail` / `upiId` — `encryptPiiFields` never descends into
179
+ // arrays, so either reaching `changes` would persist in PLAINTEXT.
180
+ piiFields: PAYOUT_PII_FIELDS,
156
181
  });
182
+ return withEntry ?? patch;
157
183
  }
158
184
  /**
159
185
  * Get all order IDs that have already been paid out for a seller.