@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.
- package/dist/_internal/server/features/bundles/actions.js +7 -0
- package/dist/_internal/server/features/checkout/actions.js +24 -16
- package/dist/_internal/server/features/checkout/locked-lines.js +12 -2
- package/dist/_internal/server/features/stores/adapters.d.ts +1 -1
- package/dist/_internal/server/features/stores/adapters.js +5 -0
- package/dist/_internal/server/jobs/core/auctionSettlement.js +10 -4
- package/dist/_internal/server/jobs/core/offerExpiry.js +10 -2
- package/dist/_internal/server/jobs/core/payoutBatch.js +15 -3
- package/dist/_internal/shared/actions/action-registry.js +41 -0
- package/dist/_internal/shared/checkout/order-math.d.ts +23 -5
- package/dist/_internal/shared/checkout/order-math.js +29 -6
- package/dist/_internal/shared/checkout/rules/_limits.d.ts +12 -2
- package/dist/_internal/shared/checkout/rules/_limits.js +12 -2
- package/dist/_internal/shared/features/cart/config.d.ts +2 -1
- package/dist/_internal/shared/features/cart/config.js +2 -1
- package/dist/_internal/shared/features/cart/schema.d.ts +6 -6
- package/dist/_internal/shared/features/notifications/action-url.d.ts +47 -0
- package/dist/_internal/shared/features/notifications/action-url.js +111 -0
- package/dist/_internal/shared/features/products/schema.d.ts +8 -8
- package/dist/_internal/shared/features/promotions/schema.d.ts +4 -4
- package/dist/_internal/shared/history/index.d.ts +1 -1
- package/dist/_internal/shared/history/types.d.ts +16 -0
- package/dist/client.d.ts +2 -0
- package/dist/client.js +1 -0
- package/dist/constants/field-names.d.ts +12 -0
- package/dist/constants/field-names.js +12 -0
- package/dist/constants/notification-types.d.ts +38 -10
- package/dist/constants/notification-types.js +39 -28
- package/dist/features/account/types/index.d.ts +1 -10
- package/dist/features/admin/actions/admin-actions.js +13 -3
- package/dist/features/admin/actions/notification-actions.d.ts +12 -0
- package/dist/features/admin/actions/notification-actions.js +149 -4
- package/dist/features/admin/components/AdminBidsView.js +7 -1
- package/dist/features/admin/components/AdminScammerEditorView.d.ts +5 -1
- package/dist/features/admin/components/AdminScammerEditorView.js +3 -2
- package/dist/features/admin/components/AdminSupportTicketDetailView.d.ts +5 -1
- package/dist/features/admin/components/AdminSupportTicketDetailView.js +3 -2
- package/dist/features/admin/schemas/firestore.d.ts +14 -3
- package/dist/features/admin/schemas/firestore.js +79 -0
- package/dist/features/auctions/repository/bid.repository.d.ts +21 -8
- package/dist/features/auctions/repository/bid.repository.js +57 -33
- package/dist/features/auctions/schemas/firestore.d.ts +27 -0
- package/dist/features/auctions/schemas/firestore.js +21 -0
- package/dist/features/auth/schemas/index.d.ts +2 -2
- package/dist/features/cart/actions/cart-actions.d.ts +16 -0
- package/dist/features/cart/actions/cart-actions.js +26 -12
- package/dist/features/cart/components/CartDrawer.js +2 -1
- package/dist/features/cart/schemas/firestore.d.ts +132 -8
- package/dist/features/cart/schemas/firestore.js +22 -0
- package/dist/features/catalogue/actions/approve-catalogue-listing.js +7 -6
- package/dist/features/catalogue/actions/list-from-catalogue.js +2 -2
- package/dist/features/catalogue/actions/submit-for-approval.js +1 -4
- package/dist/features/catalogue/components/AdminCatalogueApprovalsView.js +5 -1
- package/dist/features/catalogue/repository/catalogue.repository.d.ts +23 -0
- package/dist/features/catalogue/repository/catalogue.repository.js +28 -1
- package/dist/features/catalogue/schemas/firestore.d.ts +19 -0
- package/dist/features/catalogue/schemas/firestore.js +12 -0
- package/dist/features/categories/schemas/index.d.ts +2 -2
- package/dist/features/email/index.d.ts +1 -1
- package/dist/features/email/index.js +5 -1
- package/dist/features/email/notification-templates.d.ts +35 -0
- package/dist/features/email/notification-templates.js +96 -0
- package/dist/features/messages/schemas/index.d.ts +6 -6
- package/dist/features/orders/schemas/index.d.ts +14 -14
- package/dist/features/payments/repository/payout.repository.d.ts +18 -5
- package/dist/features/payments/repository/payout.repository.js +45 -19
- package/dist/features/payments/schemas/firestore.d.ts +33 -1
- package/dist/features/payments/schemas/firestore.js +39 -0
- package/dist/features/payments/schemas/index.d.ts +12 -12
- package/dist/features/products/schemas/index.d.ts +2 -2
- package/dist/features/promotions/schemas/index.d.ts +12 -12
- package/dist/features/scams/repository/scammer.repository.d.ts +18 -1
- package/dist/features/scams/repository/scammer.repository.js +24 -2
- package/dist/features/scams/schemas/firestore.d.ts +24 -0
- package/dist/features/scams/schemas/firestore.js +30 -0
- package/dist/features/shipments/components/AdminShipmentEditorView.js +2 -1
- package/dist/features/shipments/repository/shipments.repository.d.ts +18 -0
- package/dist/features/shipments/repository/shipments.repository.js +23 -1
- package/dist/features/shipments/schemas/firestore.d.ts +14 -0
- package/dist/features/shipments/schemas/firestore.js +19 -0
- package/dist/features/status-history/components/RecordStatusTimeline.d.ts +12 -0
- package/dist/features/status-history/components/RecordStatusTimeline.js +90 -0
- package/dist/features/status-history/index.d.ts +2 -0
- package/dist/features/status-history/index.js +1 -0
- package/dist/features/stores/repository/store.repository.d.ts +30 -2
- package/dist/features/stores/repository/store.repository.js +43 -4
- package/dist/features/stores/schemas/firestore.d.ts +29 -0
- package/dist/features/stores/schemas/firestore.js +31 -0
- package/dist/features/support/repository/support.repository.d.ts +47 -3
- package/dist/features/support/repository/support.repository.js +72 -12
- package/dist/features/support/schemas/firestore.d.ts +38 -1
- package/dist/features/support/schemas/firestore.js +34 -0
- package/dist/features/support/schemas/index.d.ts +2 -2
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +84 -0
- package/dist/index.d.ts +7 -4
- package/dist/index.js +15 -2
- package/dist/schemas/registry.d.ts +34 -34
- package/dist/seed/bids-seed-data.js +8 -0
- package/dist/seed/factories/notification.factory.d.ts +2 -1
- package/dist/server-entry.d.ts +1 -0
- package/dist/server-entry.js +5 -0
- package/dist/server.d.ts +5 -1
- package/dist/server.js +10 -1
- package/dist/styles.css +58 -1
- package/dist/tailwind-utilities.css +1 -1
- package/dist/ui/components/QuantityStepper.d.ts +38 -0
- package/dist/ui/components/QuantityStepper.js +20 -0
- package/dist/ui/components/QuantityStepper.style.css +56 -0
- package/dist/ui/components/index.style.css +1 -0
- package/dist/ui/index.d.ts +2 -0
- package/dist/ui/index.js +1 -0
- 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?: "
|
|
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?: "
|
|
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 {
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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?: "
|
|
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?: "
|
|
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?: "
|
|
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?: "
|
|
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?: "
|
|
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?: "
|
|
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
|
-
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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.
|