@mohasinac/appkit 4.62.0 → 4.62.1
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/.build-stamp.json +3 -3
- package/dist/_internal/server/features/checkout/actions.d.ts +80 -0
- package/dist/_internal/server/features/checkout/actions.js +542 -26
- package/dist/_internal/server/features/invoicing/allocate.d.ts +63 -0
- package/dist/_internal/server/features/invoicing/allocate.js +162 -0
- package/dist/_internal/server/features/invoicing/credit-note.d.ts +57 -0
- package/dist/_internal/server/features/invoicing/credit-note.js +126 -0
- package/dist/_internal/server/features/invoicing/gstr1.d.ts +74 -0
- package/dist/_internal/server/features/invoicing/gstr1.js +251 -0
- package/dist/_internal/server/features/invoicing/issue.d.ts +96 -0
- package/dist/_internal/server/features/invoicing/issue.js +202 -0
- package/dist/_internal/server/features/orders/actions.d.ts +8 -1
- package/dist/_internal/server/features/orders/actions.js +81 -8
- package/dist/_internal/server/features/orders/adapters.js +33 -0
- package/dist/_internal/server/features/orders/cancellation-refund.d.ts +15 -9
- package/dist/_internal/server/features/orders/cancellation-refund.js +15 -9
- package/dist/_internal/server/features/points/award.js +46 -4
- package/dist/_internal/server/features/refunds/actions.js +26 -0
- package/dist/_internal/server/features/shipping/courier-block.d.ts +69 -0
- package/dist/_internal/server/features/shipping/courier-block.js +239 -0
- package/dist/_internal/server/features/site-settings/adapters.d.ts +1 -1
- package/dist/_internal/server/features/site-settings/adapters.js +15 -0
- package/dist/_internal/server/features/stores/adapters.d.ts +2 -2
- package/dist/_internal/server/features/stores/adapters.js +51 -0
- package/dist/_internal/server/functions/scheduled.js +38 -1
- package/dist/_internal/server/jobs/core/autoPayoutEligibility.js +9 -2
- package/dist/_internal/server/jobs/core/dailyStatusDigest.d.ts +4 -0
- package/dist/_internal/server/jobs/core/dailyStatusDigest.js +19 -2
- package/dist/_internal/server/jobs/core/index.d.ts +2 -1
- package/dist/_internal/server/jobs/core/index.js +2 -1
- package/dist/_internal/server/jobs/core/onJobCreated.d.ts +30 -7
- package/dist/_internal/server/jobs/core/onJobCreated.js +78 -45
- package/dist/_internal/server/jobs/core/onOrderStatusChange.d.ts +7 -0
- package/dist/_internal/server/jobs/core/onOrderStatusChange.js +62 -0
- package/dist/_internal/server/jobs/core/paymentWindowTimeout.js +33 -0
- package/dist/_internal/server/jobs/core/runJobOnce.d.ts +48 -0
- package/dist/_internal/server/jobs/core/runJobOnce.js +195 -0
- package/dist/_internal/server/jobs/core/weeklyPayoutEligibility.js +10 -2
- package/dist/_internal/server/jobs/handlers/onOrderStatusChange.js +17 -0
- package/dist/_internal/server/notifications/channel-health.d.ts +26 -3
- package/dist/_internal/shared/actions/action-registry.js +28 -0
- package/dist/_internal/shared/checkout/coupon-discount.d.ts +43 -1
- package/dist/_internal/shared/checkout/coupon-discount.js +26 -1
- package/dist/_internal/shared/checkout/order-math.d.ts +145 -0
- package/dist/_internal/shared/checkout/order-math.js +173 -0
- package/dist/_internal/shared/checkout/refundable-base.d.ts +25 -0
- package/dist/_internal/shared/checkout/refundable-base.js +13 -1
- package/dist/_internal/shared/checkout/split.d.ts +87 -0
- package/dist/_internal/shared/checkout/split.js +100 -0
- package/dist/_internal/shared/content/disclaimers.d.ts +88 -0
- package/dist/_internal/shared/content/disclaimers.js +217 -0
- package/dist/_internal/shared/content/published-values.d.ts +88 -0
- package/dist/_internal/shared/content/published-values.js +198 -0
- package/dist/_internal/shared/features/admin/integrations.d.ts +109 -0
- package/dist/_internal/shared/features/admin/integrations.js +253 -0
- package/dist/_internal/shared/features/orders/dispute-reasons.d.ts +113 -0
- package/dist/_internal/shared/features/orders/dispute-reasons.js +181 -0
- package/dist/_internal/shared/features/orders/refund-copy.d.ts +1 -1
- package/dist/_internal/shared/features/orders/refund-copy.js +18 -1
- package/dist/_internal/shared/features/points/accepts.d.ts +36 -0
- package/dist/_internal/shared/features/points/accepts.js +43 -0
- package/dist/_internal/shared/features/shipping/shared-purchase.d.ts +91 -0
- package/dist/_internal/shared/features/shipping/shared-purchase.js +108 -0
- package/dist/_internal/shared/features/site-settings/types.d.ts +11 -0
- package/dist/_internal/shared/fees/calculator.d.ts +33 -1
- package/dist/_internal/shared/fees/calculator.js +32 -2
- package/dist/client.d.ts +11 -0
- package/dist/client.js +30 -0
- package/dist/constants/api-endpoints.d.ts +42 -0
- package/dist/constants/api-endpoints.js +14 -0
- package/dist/constants/field-names.d.ts +1 -0
- package/dist/constants/field-names.js +5 -0
- package/dist/contracts/index.d.ts +1 -1
- package/dist/contracts/shipping.d.ts +45 -0
- package/dist/contracts/shipping.js +15 -0
- package/dist/features/about/components/AboutView.js +1 -1
- package/dist/features/account/components/BuyerAuctionsGuideView.js +4 -1
- package/dist/features/account/components/UserOrdersView.js +19 -33
- package/dist/features/admin/components/AdminCouponEditorView.js +17 -0
- package/dist/features/admin/components/AdminInvoicingView.d.ts +2 -0
- package/dist/features/admin/components/AdminInvoicingView.js +65 -0
- package/dist/features/admin/components/AdminOrderEditorView.d.ts +18 -1
- package/dist/features/admin/components/AdminOrderEditorView.js +11 -2
- package/dist/features/admin/components/AdminOrdersGuideView.js +23 -2
- package/dist/features/admin/components/AdminOrdersView.js +67 -5
- package/dist/features/admin/components/OwnerQuickActions.js +24 -2
- package/dist/features/admin/constants/filter-tabs.d.ts +6 -0
- package/dist/features/admin/constants/filter-tabs.js +6 -0
- package/dist/features/admin/constants/site-settings-tabs.d.ts +1 -1
- package/dist/features/admin/hooks/useAdminListingData.js +16 -0
- package/dist/features/admin/schemas/admin-user-form.d.ts +2 -2
- package/dist/features/admin/schemas/firestore.d.ts +30 -0
- package/dist/features/admin/schemas/firestore.js +5 -0
- package/dist/features/audit-log/schemas/firestore.d.ts +30 -0
- package/dist/features/audit-log/schemas/firestore.js +15 -0
- package/dist/features/auth/permissions/constants.js +13 -0
- package/dist/features/cart/components/CartPriceBreakdown.d.ts +13 -0
- package/dist/features/cart/components/CartPriceBreakdown.js +6 -2
- package/dist/features/cart/components/PointsTenderPicker.d.ts +61 -0
- package/dist/features/cart/components/PointsTenderPicker.js +15 -0
- package/dist/features/cart/components/StoreAddonsPicker.js +2 -1
- package/dist/features/cart/components/index.d.ts +2 -0
- package/dist/features/cart/components/index.js +1 -0
- package/dist/features/cart/repository/cart.repository.d.ts +19 -0
- package/dist/features/cart/repository/cart.repository.js +27 -0
- package/dist/features/cart/schemas/firestore.d.ts +92 -0
- package/dist/features/contact/email.d.ts +27 -10
- package/dist/features/contact/email.js +20 -3
- package/dist/features/dispute-reasons/repository/dispute-reasons.repository.d.ts +38 -0
- package/dist/features/dispute-reasons/repository/dispute-reasons.repository.js +80 -0
- package/dist/features/dispute-reasons/schemas/firestore.d.ts +64 -0
- package/dist/features/dispute-reasons/schemas/firestore.js +40 -0
- package/dist/features/faq/components/FAQAccordion.d.ts +13 -1
- package/dist/features/faq/components/FAQAccordion.js +29 -4
- package/dist/features/invoicing/schemas/firestore.d.ts +112 -0
- package/dist/features/invoicing/schemas/firestore.js +45 -0
- package/dist/features/invoicing/utils/financial-year.d.ts +43 -0
- package/dist/features/invoicing/utils/financial-year.js +53 -0
- package/dist/features/jobs/actions/enqueue-job.js +18 -1
- package/dist/features/jobs/repository/jobs.repository.d.ts +62 -1
- package/dist/features/jobs/repository/jobs.repository.js +84 -1
- package/dist/features/jobs/schemas/firestore.d.ts +80 -0
- package/dist/features/jobs/schemas/firestore.js +37 -0
- package/dist/features/lottery/schemas/zod.d.ts +6 -6
- package/dist/features/media/MediaImage.js +22 -1
- package/dist/features/media/upload/ImageUpload.js +1 -1
- package/dist/features/obligations/constants/policies.js +24 -1
- package/dist/features/orders/components/OrderPaymentSummary.js +10 -1
- package/dist/features/orders/components/OrderProvenanceCard.d.ts +8 -0
- package/dist/features/orders/components/OrderProvenanceCard.js +185 -0
- package/dist/features/orders/components/RaiseDisputeForm.d.ts +14 -0
- package/dist/features/orders/components/RaiseDisputeForm.js +111 -0
- package/dist/features/orders/components/RefundRequestView.js +19 -1
- package/dist/features/orders/components/index.d.ts +4 -0
- package/dist/features/orders/components/index.js +2 -0
- package/dist/features/orders/constants/order-lane-tabs.d.ts +69 -0
- package/dist/features/orders/constants/order-lane-tabs.js +88 -0
- package/dist/features/orders/repository/orders.repository.d.ts +57 -0
- package/dist/features/orders/repository/orders.repository.js +145 -0
- package/dist/features/orders/schemas/firestore.d.ts +196 -1
- package/dist/features/orders/schemas/index.d.ts +54 -6
- package/dist/features/orders/schemas/index.js +12 -0
- package/dist/features/orders/types/index.d.ts +26 -0
- package/dist/features/orders/utils/courier-block.d.ts +25 -0
- package/dist/features/orders/utils/courier-block.js +23 -0
- package/dist/features/payment-cases/schemas/firestore.d.ts +26 -1
- package/dist/features/payment-cases/schemas/firestore.js +1 -0
- package/dist/features/products/components/PrizeDrawDetailPageView.js +2 -2
- package/dist/features/products/components/ProductDetailPageView.d.ts +17 -1
- package/dist/features/products/components/ProductDetailPageView.js +2 -2
- package/dist/features/products/schemas/product-features.validators.d.ts +10 -10
- package/dist/features/promotions/actions/coupon-stacking.d.ts +30 -3
- package/dist/features/promotions/actions/coupon-stacking.js +21 -2
- package/dist/features/promotions/components/CouponCard.js +13 -0
- package/dist/features/promotions/constants/coupon-help.d.ts +2 -2
- package/dist/features/promotions/constants/coupon-help.js +9 -1
- package/dist/features/promotions/schemas/firestore.d.ts +24 -0
- package/dist/features/promotions/schemas/firestore.js +4 -0
- package/dist/features/promotions/types/index.d.ts +19 -1
- package/dist/features/scams/components/ScamProfileView.js +2 -2
- package/dist/features/scams/components/ScamRegistryView.js +2 -2
- package/dist/features/search/action-index/tab-entries.js +1 -1
- package/dist/features/seller/actions/seller-actions.js +33 -3
- package/dist/features/seller/components/SellerOrdersView.js +129 -27
- package/dist/features/seller/hooks/useSellerListingData.js +25 -0
- package/dist/features/shipments/schemas/validation.d.ts +4 -4
- package/dist/features/shipping-boxes/repository/shipping-boxes.repository.d.ts +43 -0
- package/dist/features/shipping-boxes/repository/shipping-boxes.repository.js +90 -0
- package/dist/features/shipping-boxes/schemas/firestore.d.ts +82 -0
- package/dist/features/shipping-boxes/schemas/firestore.js +53 -0
- package/dist/features/store-extensions/schemas/report-create-form.d.ts +2 -2
- package/dist/features/store-extensions/schemas/shipping-config-form.d.ts +2 -2
- package/dist/features/stores/components/StoreOrdersGuideView.js +6 -1
- package/dist/features/stores/schemas/firestore.d.ts +71 -0
- package/dist/features/stores/types/index.d.ts +19 -0
- package/dist/features/support/schemas/index.d.ts +6 -6
- package/dist/features/wishlist/hooks/useWishlistWithGuest.d.ts +1 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.js +14 -1
- package/dist/next/routing/route-map.d.ts +20 -0
- package/dist/next/routing/route-map.js +10 -0
- package/dist/providers/shipping-shiprocket/index.d.ts +27 -1
- package/dist/providers/shipping-shiprocket/index.js +84 -1
- package/dist/schemas/registry.d.ts +47 -18
- package/dist/schemas/sieve.d.ts +2 -2
- package/dist/server-entry.d.ts +2 -2
- package/dist/server-entry.js +8 -1
- package/dist/server.d.ts +10 -1
- package/dist/server.js +39 -1
- package/dist/styles.css +64 -80
- package/dist/tailwind-utilities.css +1 -1
- package/dist/tokens/themes/default-dark.js +1 -0
- package/dist/tokens/themes/default-light.js +1 -0
- package/dist/tokens/themes/required.d.ts +1 -1
- package/dist/tokens/themes/required.js +1 -0
- package/dist/tokens/tokens.css +15 -0
- package/dist/ui/components/Disclaimer.d.ts +19 -0
- package/dist/ui/components/Disclaimer.js +74 -0
- package/dist/ui/components/tabs-overflow.js +64 -11
- package/dist/ui/index.d.ts +2 -0
- package/dist/ui/index.js +6 -0
- package/dist/ui/rich-text/RichText.style.css +48 -79
- package/package.json +1 -1
- package/scripts/seed-cli.mjs +1831 -1822
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GSTR-1 export (Phase 7d) — the INPUT to a human filing, never a filing.
|
|
3
|
+
*
|
|
4
|
+
* ## 🛑 What this is and is not
|
|
5
|
+
*
|
|
6
|
+
* It is a CSV an accountant opens, checks and uses. It is not a return, it is
|
|
7
|
+
* not filed, and nothing here talks to the GST portal — there is no webhook
|
|
8
|
+
* facility in existence and no GSP contract. Labelling it otherwise would be
|
|
9
|
+
* the most consequential lie in this codebase.
|
|
10
|
+
*
|
|
11
|
+
* ## 🛑 Credit notes are not optional
|
|
12
|
+
*
|
|
13
|
+
* A return listing invoices and no credit notes **does not reconcile**: the
|
|
14
|
+
* output tax declared would be the tax on every sale including the ones that
|
|
15
|
+
* were reversed. So both series are exported, the credit notes as negative
|
|
16
|
+
* amounts, and the file's own total is the net.
|
|
17
|
+
*
|
|
18
|
+
* That is also why the two series exist in the first place — a reversal is its
|
|
19
|
+
* own numbered document, so it has its own row here.
|
|
20
|
+
*
|
|
21
|
+
* ## The rate split comes from `gstByRate`
|
|
22
|
+
*
|
|
23
|
+
* Already on the order (`firestore.ts`), already per rate, already recorded at
|
|
24
|
+
* the moment of supply. Nothing is recomputed from today's tax codes: an order
|
|
25
|
+
* supplied under a rate that has since changed must appear at the rate it was
|
|
26
|
+
* charged, and re-deriving it would silently restate history — the same defect
|
|
27
|
+
* the PDF renderer's own 🛑 records about reading live site settings.
|
|
28
|
+
*/
|
|
29
|
+
import { orderRepository } from "../../../../features/orders/repository/orders.repository.js";
|
|
30
|
+
import { roundRupees } from "../../../../utils/number.formatter.js";
|
|
31
|
+
import { serverLogger } from "../../../../monitoring/index.js";
|
|
32
|
+
/** One row per (document, rate) slice — which is what a GSTR-1 line is. */
|
|
33
|
+
function rowsForOrder(order) {
|
|
34
|
+
/*
|
|
35
|
+
* 🛑 `placeOfSupply` ONLY — never parsed out of `shippingAddress`, which is
|
|
36
|
+
* a flat string on this document. Deriving a state from free text to file a
|
|
37
|
+
* tax return is not a thing to do, and an order placed before the field
|
|
38
|
+
* existed is reported as blank rather than guessed at.
|
|
39
|
+
*/
|
|
40
|
+
const placeOfSupply = order.placeOfSupply ?? "";
|
|
41
|
+
const date = order.invoiceIssuedAt ?? order.orderDate ?? order.createdAt;
|
|
42
|
+
const dateStr = date instanceof Date ? date.toISOString().slice(0, 10) : "";
|
|
43
|
+
const byRate = order.gstByRate ?? [];
|
|
44
|
+
const invoiceRows = order.invoiceNumber
|
|
45
|
+
? byRate.map((slice) => ({
|
|
46
|
+
documentNumber: order.invoiceNumber,
|
|
47
|
+
documentType: "invoice",
|
|
48
|
+
documentDate: dateStr,
|
|
49
|
+
orderId: order.id,
|
|
50
|
+
placeOfSupply,
|
|
51
|
+
gstRate: slice.gstRate,
|
|
52
|
+
taxableValue: roundRupees(slice.taxableAmount),
|
|
53
|
+
cgst: roundRupees(slice.cgst),
|
|
54
|
+
sgst: roundRupees(slice.sgst),
|
|
55
|
+
igst: roundRupees(slice.igst),
|
|
56
|
+
total: roundRupees(slice.taxableAmount + slice.cgst + slice.sgst + slice.igst),
|
|
57
|
+
}))
|
|
58
|
+
: [];
|
|
59
|
+
/*
|
|
60
|
+
* ── Credit notes, as NEGATIVE rows ────────────────────────────────────────
|
|
61
|
+
*
|
|
62
|
+
* 🛑 The tax reversed is apportioned across the SAME rate slices the invoice
|
|
63
|
+
* carried, pro-rata on the refunded amount. A refund is a rupee figure with
|
|
64
|
+
* no rate attached, and a multi-rate order's reversal has to be split
|
|
65
|
+
* somehow — pro-rata on the invoice's own slices is the only split that
|
|
66
|
+
* reconciles back to the invoice it adjusts.
|
|
67
|
+
*
|
|
68
|
+
* 🛑 Only SETTLED refunds appear, and only ones that were numbered. A
|
|
69
|
+
* `reserved` entry is money that has not moved; a numbered reversal for it
|
|
70
|
+
* would overstate the period's credit notes.
|
|
71
|
+
*/
|
|
72
|
+
const invoiceTotal = byRate.reduce((s, r) => s + r.taxableAmount, 0);
|
|
73
|
+
const creditRows = [];
|
|
74
|
+
for (const refund of order.refunds ?? []) {
|
|
75
|
+
if (!refund.creditNoteNumber)
|
|
76
|
+
continue;
|
|
77
|
+
if (refund.status === "reserved" || refund.status === "failed")
|
|
78
|
+
continue;
|
|
79
|
+
const refundDate = refund.creditNoteIssuedAt ?? refund.refundedAt;
|
|
80
|
+
const refundDateStr = refundDate instanceof Date ? refundDate.toISOString().slice(0, 10) : dateStr;
|
|
81
|
+
// No rate information at all — reported as one untaxed row rather than
|
|
82
|
+
// dropped, so the CSV's total still matches the money that moved.
|
|
83
|
+
if (invoiceTotal <= 0 || byRate.length === 0) {
|
|
84
|
+
creditRows.push({
|
|
85
|
+
documentNumber: refund.creditNoteNumber,
|
|
86
|
+
documentType: "credit_note",
|
|
87
|
+
documentDate: refundDateStr,
|
|
88
|
+
orderId: order.id,
|
|
89
|
+
placeOfSupply,
|
|
90
|
+
gstRate: 0,
|
|
91
|
+
taxableValue: -roundRupees(refund.amount),
|
|
92
|
+
cgst: 0,
|
|
93
|
+
sgst: 0,
|
|
94
|
+
igst: 0,
|
|
95
|
+
total: -roundRupees(refund.amount),
|
|
96
|
+
});
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
for (const slice of byRate) {
|
|
100
|
+
/*
|
|
101
|
+
* The slice's share of the refund, and then that amount's share of the
|
|
102
|
+
* slice — two ratios, because a refund is a gross rupee figure while a
|
|
103
|
+
* slice is split into taxable value and tax.
|
|
104
|
+
*
|
|
105
|
+
* 🛑 `sliceTotal > 0` guards a 0%-rated (exempt) slice, where the tax is
|
|
106
|
+
* legitimately zero and a naive `portion / sliceTotal` would be a
|
|
107
|
+
* division by zero producing `Infinity` — and `roundRupees(Infinity)` is
|
|
108
|
+
* `NaN`, which propagates into the file's TOTAL row and makes every
|
|
109
|
+
* figure in it unusable. One bad cell would be visible; a NaN total is
|
|
110
|
+
* the whole return.
|
|
111
|
+
*/
|
|
112
|
+
const share = slice.taxableAmount / invoiceTotal;
|
|
113
|
+
const portion = refund.amount * share;
|
|
114
|
+
const sliceTotal = slice.taxableAmount + slice.cgst + slice.sgst + slice.igst;
|
|
115
|
+
const taxFraction = sliceTotal > 0 ? portion / sliceTotal : 0;
|
|
116
|
+
creditRows.push({
|
|
117
|
+
documentNumber: refund.creditNoteNumber,
|
|
118
|
+
documentType: "credit_note",
|
|
119
|
+
documentDate: refundDateStr,
|
|
120
|
+
orderId: order.id,
|
|
121
|
+
placeOfSupply,
|
|
122
|
+
gstRate: slice.gstRate,
|
|
123
|
+
taxableValue: -roundRupees(slice.taxableAmount * taxFraction),
|
|
124
|
+
cgst: -roundRupees(slice.cgst * taxFraction),
|
|
125
|
+
sgst: -roundRupees(slice.sgst * taxFraction),
|
|
126
|
+
igst: -roundRupees(slice.igst * taxFraction),
|
|
127
|
+
total: -roundRupees(portion),
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return [...invoiceRows, ...creditRows];
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Build the export for a date window.
|
|
135
|
+
*
|
|
136
|
+
* 🛑 Windowed on the INVOICE date, not the order date. A COD order placed on
|
|
137
|
+
* 30 March and paid on 2 April is invoiced in the new financial year, and that
|
|
138
|
+
* is the period its tax belongs to — numbering happens at payment for exactly
|
|
139
|
+
* this reason.
|
|
140
|
+
*/
|
|
141
|
+
export async function buildGstr1Export(from, to) {
|
|
142
|
+
const orders = await orderRepository.listInvoicedBetween(from, to);
|
|
143
|
+
const rows = [];
|
|
144
|
+
const unnumberedPaidOrders = [];
|
|
145
|
+
for (const order of orders) {
|
|
146
|
+
if (order.paymentStatus === "paid" && !order.invoiceNumber) {
|
|
147
|
+
unnumberedPaidOrders.push(order.id);
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
rows.push(...rowsForOrder(order));
|
|
151
|
+
}
|
|
152
|
+
const totals = rows.reduce((acc, r) => ({
|
|
153
|
+
taxableValue: acc.taxableValue + r.taxableValue,
|
|
154
|
+
cgst: acc.cgst + r.cgst,
|
|
155
|
+
sgst: acc.sgst + r.sgst,
|
|
156
|
+
igst: acc.igst + r.igst,
|
|
157
|
+
total: acc.total + r.total,
|
|
158
|
+
}), { taxableValue: 0, cgst: 0, sgst: 0, igst: 0, total: 0 });
|
|
159
|
+
if (unnumberedPaidOrders.length > 0) {
|
|
160
|
+
serverLogger.warn("[gstr1] paid orders with no invoice number in window", {
|
|
161
|
+
count: unnumberedPaidOrders.length,
|
|
162
|
+
from: from.toISOString(),
|
|
163
|
+
to: to.toISOString(),
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
return {
|
|
167
|
+
rows,
|
|
168
|
+
totals: {
|
|
169
|
+
taxableValue: roundRupees(totals.taxableValue),
|
|
170
|
+
cgst: roundRupees(totals.cgst),
|
|
171
|
+
sgst: roundRupees(totals.sgst),
|
|
172
|
+
igst: roundRupees(totals.igst),
|
|
173
|
+
total: roundRupees(totals.total),
|
|
174
|
+
},
|
|
175
|
+
unnumberedPaidOrders,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
const CSV_HEADER = [
|
|
179
|
+
"Document Number",
|
|
180
|
+
"Document Type",
|
|
181
|
+
"Document Date",
|
|
182
|
+
"Order ID",
|
|
183
|
+
"Place of Supply",
|
|
184
|
+
"GST Rate",
|
|
185
|
+
"Taxable Value",
|
|
186
|
+
"CGST",
|
|
187
|
+
"SGST",
|
|
188
|
+
"IGST",
|
|
189
|
+
"Total",
|
|
190
|
+
].join(",");
|
|
191
|
+
/**
|
|
192
|
+
* 🛑 Every field is quoted and internal quotes are doubled.
|
|
193
|
+
*
|
|
194
|
+
* An invoice number contains `/`, a place of supply can contain a comma, and a
|
|
195
|
+
* CSV whose columns shift by one on a single row is worse than no CSV — it
|
|
196
|
+
* reconciles to a plausible wrong number rather than failing to open.
|
|
197
|
+
*/
|
|
198
|
+
function csvCell(value) {
|
|
199
|
+
const s = String(value);
|
|
200
|
+
return `"${s.replace(/"/g, '""')}"`;
|
|
201
|
+
}
|
|
202
|
+
export function gstr1ToCsv(data) {
|
|
203
|
+
const lines = [CSV_HEADER];
|
|
204
|
+
for (const r of data.rows) {
|
|
205
|
+
lines.push([
|
|
206
|
+
r.documentNumber,
|
|
207
|
+
r.documentType === "credit_note" ? "Credit Note" : "Invoice",
|
|
208
|
+
r.documentDate,
|
|
209
|
+
r.orderId,
|
|
210
|
+
r.placeOfSupply,
|
|
211
|
+
r.gstRate,
|
|
212
|
+
r.taxableValue.toFixed(2),
|
|
213
|
+
r.cgst.toFixed(2),
|
|
214
|
+
r.sgst.toFixed(2),
|
|
215
|
+
r.igst.toFixed(2),
|
|
216
|
+
r.total.toFixed(2),
|
|
217
|
+
]
|
|
218
|
+
.map(csvCell)
|
|
219
|
+
.join(","));
|
|
220
|
+
}
|
|
221
|
+
/*
|
|
222
|
+
* A TOTAL row, and it is net of credit notes. An accountant's first act is to
|
|
223
|
+
* check the file's own total against their own figure; making them sum 400
|
|
224
|
+
* rows to do it is how a transcription error enters a tax return.
|
|
225
|
+
*/
|
|
226
|
+
lines.push([
|
|
227
|
+
"TOTAL",
|
|
228
|
+
"",
|
|
229
|
+
"",
|
|
230
|
+
"",
|
|
231
|
+
"",
|
|
232
|
+
"",
|
|
233
|
+
data.totals.taxableValue.toFixed(2),
|
|
234
|
+
data.totals.cgst.toFixed(2),
|
|
235
|
+
data.totals.sgst.toFixed(2),
|
|
236
|
+
data.totals.igst.toFixed(2),
|
|
237
|
+
data.totals.total.toFixed(2),
|
|
238
|
+
]
|
|
239
|
+
.map(csvCell)
|
|
240
|
+
.join(","));
|
|
241
|
+
/*
|
|
242
|
+
* 🛑 The exceptions go IN the file, not only in a log. The person filing is
|
|
243
|
+
* not the person reading our logs, and a paid order absent from a tax return
|
|
244
|
+
* is the one error this export must not hide.
|
|
245
|
+
*/
|
|
246
|
+
if (data.unnumberedPaidOrders.length > 0) {
|
|
247
|
+
lines.push("");
|
|
248
|
+
lines.push(csvCell(`WARNING: ${data.unnumberedPaidOrders.length} paid order(s) in this window carry no invoice number and are NOT included above: ${data.unnumberedPaidOrders.join(" ")}`));
|
|
249
|
+
}
|
|
250
|
+
return lines.join("\n");
|
|
251
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Issuing an invoice: allocate the number, render, store the bytes (Phase 7).
|
|
3
|
+
*
|
|
4
|
+
* ## 🛑 WHERE the PDF goes, and why it is not the media path
|
|
5
|
+
*
|
|
6
|
+
* `private/invoices/{orderId}/{uuid}.pdf`, with **no `mediaAssets` row** and a
|
|
7
|
+
* **random** object name. An invoice carries the buyer's full name, address and
|
|
8
|
+
* phone.
|
|
9
|
+
*
|
|
10
|
+
* Three facts, each of which alone would be enough:
|
|
11
|
+
*
|
|
12
|
+
* 1. `/api/media/[...slug]` applies **no authentication at all** — it is a
|
|
13
|
+
* deliberately bare handler whose job is watermarking public product
|
|
14
|
+
* photography.
|
|
15
|
+
* 2. The bucket's storage rule is `allow read: if true`.
|
|
16
|
+
* 3. Media filenames in this codebase are content-derived and therefore
|
|
17
|
+
* GUESSABLE — `generateInvoiceFilename` is `invoice-{orderId}-{YYYYMMDD}`.
|
|
18
|
+
*
|
|
19
|
+
* So the random name is against the SEO-slug convention everywhere else *on
|
|
20
|
+
* purpose*: guessable is the one property this must not have. And the `private/`
|
|
21
|
+
* prefix is refused by the proxy's raw-path fallback — a check, not a naming
|
|
22
|
+
* convention, which is precisely the distinction the digital-content comment
|
|
23
|
+
* got wrong for weeks.
|
|
24
|
+
*
|
|
25
|
+
* ## 🛑 Store the bytes; never re-render to serve or to email
|
|
26
|
+
*
|
|
27
|
+
* Two renders can disagree — a changed template, a different library version,
|
|
28
|
+
* a settings read that moved — and the buyer's copy must be byte-identical to
|
|
29
|
+
* ours. So the bytes are written once, at issue time, and every later consumer
|
|
30
|
+
* reads that object. On-demand rendering survives only as a fallback for orders
|
|
31
|
+
* that predate this phase and have no stored path.
|
|
32
|
+
*/
|
|
33
|
+
import type { OrderDocument } from "../../../../features/orders/schemas/firestore";
|
|
34
|
+
/** Storage prefix. Mirrors `private/digital-content/` — refused by the proxy. */
|
|
35
|
+
export declare const INVOICE_PREFIX_PATH = "private/invoices";
|
|
36
|
+
/**
|
|
37
|
+
* Renders the PDF for an order.
|
|
38
|
+
*
|
|
39
|
+
* Injected rather than imported, because the renderer lives in
|
|
40
|
+
* `functions/src/invoice-pdf.ts` — it pulls `pdfkit`, which is a Functions
|
|
41
|
+
* dependency and has no business in appkit's module graph (Rule #6's build
|
|
42
|
+
* weight, and Root Cause #18's import-chain trap).
|
|
43
|
+
*/
|
|
44
|
+
export type InvoiceRenderer = (order: OrderDocument) => Promise<Buffer>;
|
|
45
|
+
export interface IssueInvoiceResult {
|
|
46
|
+
invoiceNumber: string;
|
|
47
|
+
invoiceStoragePath: string;
|
|
48
|
+
/** False when the order already had an invoice — the normal re-entry case. */
|
|
49
|
+
issued: boolean;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Give a paid order its invoice number and its stored PDF.
|
|
53
|
+
*
|
|
54
|
+
* 🛑 Idempotent through `allocateSeriesNumber`'s transactional claim.
|
|
55
|
+
* `onOrderStatusChange` can fire more than once, and two numbers for one order
|
|
56
|
+
* is the single unrecoverable failure in this phase — a number cannot be
|
|
57
|
+
* withdrawn without leaving a gap.
|
|
58
|
+
*
|
|
59
|
+
* 🛑 Called on `paymentStatus → paid`, never at creation. COD and manual orders
|
|
60
|
+
* are created `pending`.
|
|
61
|
+
*/
|
|
62
|
+
export declare function issueInvoiceForOrder(order: OrderDocument, render: InvoiceRenderer): Promise<IssueInvoiceResult>;
|
|
63
|
+
/**
|
|
64
|
+
* Persist bytes rendered on demand, for an order that is already NUMBERED.
|
|
65
|
+
*
|
|
66
|
+
* ## Why the lazy path exists at all
|
|
67
|
+
*
|
|
68
|
+
* Numbering happens in `onOrderStatusChange`, which runs in appkit and cannot
|
|
69
|
+
* render — `pdfkit` is a Functions dependency and bringing it into appkit's
|
|
70
|
+
* module graph would add it to every consumer build (Rule #6's build weight,
|
|
71
|
+
* Root Cause #18's import chain). So the two halves are split: the number is
|
|
72
|
+
* allocated immediately and transactionally, the bytes are produced the first
|
|
73
|
+
* time anyone asks for them and then stored forever.
|
|
74
|
+
*
|
|
75
|
+
* That split is a deliberate trade and worth naming: the first download of a
|
|
76
|
+
* given invoice pays a render, and every later one does not. The alternative —
|
|
77
|
+
* a second Firestore trigger on `orders` living in `functions/` — is the shape
|
|
78
|
+
* that burned a million invocations a day in Root Cause #92.
|
|
79
|
+
*
|
|
80
|
+
* 🛑 Refuses to overwrite an existing path. A second numbered copy of one
|
|
81
|
+
* invoice is two documents claiming one number, and the one a buyer already
|
|
82
|
+
* downloaded is the one that counts.
|
|
83
|
+
*
|
|
84
|
+
* 🛑 Refuses an un-numbered order. Storing a PDF before the number exists
|
|
85
|
+
* would freeze a pre-payment snapshot as "the invoice", and the real issuance
|
|
86
|
+
* would then find a path already set.
|
|
87
|
+
*/
|
|
88
|
+
export declare function storeRenderedInvoice(orderId: string, bytes: Buffer): Promise<string | null>;
|
|
89
|
+
/**
|
|
90
|
+
* The stored bytes for an already-issued invoice, or null.
|
|
91
|
+
*
|
|
92
|
+
* Returns null rather than throwing on a missing object, because the caller's
|
|
93
|
+
* correct response is to fall back to an on-demand render for a pre-Phase-7
|
|
94
|
+
* order — and a throw would turn a legacy order's invoice page into an error.
|
|
95
|
+
*/
|
|
96
|
+
export declare function readStoredInvoice(storagePath: string | undefined): Promise<Buffer | null>;
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Issuing an invoice: allocate the number, render, store the bytes (Phase 7).
|
|
3
|
+
*
|
|
4
|
+
* ## 🛑 WHERE the PDF goes, and why it is not the media path
|
|
5
|
+
*
|
|
6
|
+
* `private/invoices/{orderId}/{uuid}.pdf`, with **no `mediaAssets` row** and a
|
|
7
|
+
* **random** object name. An invoice carries the buyer's full name, address and
|
|
8
|
+
* phone.
|
|
9
|
+
*
|
|
10
|
+
* Three facts, each of which alone would be enough:
|
|
11
|
+
*
|
|
12
|
+
* 1. `/api/media/[...slug]` applies **no authentication at all** — it is a
|
|
13
|
+
* deliberately bare handler whose job is watermarking public product
|
|
14
|
+
* photography.
|
|
15
|
+
* 2. The bucket's storage rule is `allow read: if true`.
|
|
16
|
+
* 3. Media filenames in this codebase are content-derived and therefore
|
|
17
|
+
* GUESSABLE — `generateInvoiceFilename` is `invoice-{orderId}-{YYYYMMDD}`.
|
|
18
|
+
*
|
|
19
|
+
* So the random name is against the SEO-slug convention everywhere else *on
|
|
20
|
+
* purpose*: guessable is the one property this must not have. And the `private/`
|
|
21
|
+
* prefix is refused by the proxy's raw-path fallback — a check, not a naming
|
|
22
|
+
* convention, which is precisely the distinction the digital-content comment
|
|
23
|
+
* got wrong for weeks.
|
|
24
|
+
*
|
|
25
|
+
* ## 🛑 Store the bytes; never re-render to serve or to email
|
|
26
|
+
*
|
|
27
|
+
* Two renders can disagree — a changed template, a different library version,
|
|
28
|
+
* a settings read that moved — and the buyer's copy must be byte-identical to
|
|
29
|
+
* ours. So the bytes are written once, at issue time, and every later consumer
|
|
30
|
+
* reads that object. On-demand rendering survives only as a fallback for orders
|
|
31
|
+
* that predate this phase and have no stored path.
|
|
32
|
+
*/
|
|
33
|
+
import { randomUUID } from "node:crypto";
|
|
34
|
+
import { getAdminStorage } from "../../../../providers/db-firebase/index.js";
|
|
35
|
+
import { orderRepository } from "../../../../features/orders/repository/orders.repository.js";
|
|
36
|
+
import { serverLogger } from "../../../../monitoring/index.js";
|
|
37
|
+
import { normalizeError } from "../../../../errors/normalize.js";
|
|
38
|
+
import { allocateSeriesNumber } from "./allocate.js";
|
|
39
|
+
/** Storage prefix. Mirrors `private/digital-content/` — refused by the proxy. */
|
|
40
|
+
export const INVOICE_PREFIX_PATH = "private/invoices";
|
|
41
|
+
/**
|
|
42
|
+
* Give a paid order its invoice number and its stored PDF.
|
|
43
|
+
*
|
|
44
|
+
* 🛑 Idempotent through `allocateSeriesNumber`'s transactional claim.
|
|
45
|
+
* `onOrderStatusChange` can fire more than once, and two numbers for one order
|
|
46
|
+
* is the single unrecoverable failure in this phase — a number cannot be
|
|
47
|
+
* withdrawn without leaving a gap.
|
|
48
|
+
*
|
|
49
|
+
* 🛑 Called on `paymentStatus → paid`, never at creation. COD and manual orders
|
|
50
|
+
* are created `pending`.
|
|
51
|
+
*/
|
|
52
|
+
export async function issueInvoiceForOrder(order, render) {
|
|
53
|
+
/*
|
|
54
|
+
* The fast path, and the common one: a second trigger on an order that is
|
|
55
|
+
* already invoiced does no work and touches no counter. Checked before the
|
|
56
|
+
* transaction purely to save the round trip — the transaction would reach
|
|
57
|
+
* the same answer, which is what makes this safe rather than a race.
|
|
58
|
+
*/
|
|
59
|
+
if (order.invoiceNumber && order.invoiceStoragePath) {
|
|
60
|
+
return {
|
|
61
|
+
invoiceNumber: order.invoiceNumber,
|
|
62
|
+
invoiceStoragePath: order.invoiceStoragePath,
|
|
63
|
+
issued: false,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
const issuedAt = new Date();
|
|
67
|
+
const allocation = await allocateSeriesNumber({
|
|
68
|
+
kind: "invoice",
|
|
69
|
+
subjectId: order.id,
|
|
70
|
+
orderId: order.id,
|
|
71
|
+
at: issuedAt,
|
|
72
|
+
});
|
|
73
|
+
/*
|
|
74
|
+
* 🛑 `randomUUID()`, and the ORDER ID is in the directory rather than the
|
|
75
|
+
* filename. The directory makes an operator's life possible (every document
|
|
76
|
+
* for one order is in one place); the random leaf is what makes the object
|
|
77
|
+
* unguessable. Putting the order id in the FILENAME would undo the second
|
|
78
|
+
* without improving the first.
|
|
79
|
+
*/
|
|
80
|
+
const storagePath = `${INVOICE_PREFIX_PATH}/${order.id}/${randomUUID()}.pdf`;
|
|
81
|
+
/*
|
|
82
|
+
* Rendered with the number already on the order object, so the PDF carries
|
|
83
|
+
* the number it was allocated — the renderer reads `order.invoiceNumber`,
|
|
84
|
+
* and handing it the un-numbered document would produce an invoice with no
|
|
85
|
+
* number printed on it while the database said otherwise.
|
|
86
|
+
*/
|
|
87
|
+
const buffer = await render({
|
|
88
|
+
...order,
|
|
89
|
+
invoiceNumber: allocation.number,
|
|
90
|
+
invoiceIssuedAt: issuedAt,
|
|
91
|
+
});
|
|
92
|
+
await getAdminStorage()
|
|
93
|
+
.bucket()
|
|
94
|
+
.file(storagePath)
|
|
95
|
+
.save(buffer, {
|
|
96
|
+
contentType: "application/pdf",
|
|
97
|
+
/*
|
|
98
|
+
* 🛑 `private, no-store` on the OBJECT as well as on every response that
|
|
99
|
+
* serves it. The object is not reachable without a signed URL or the
|
|
100
|
+
* Admin SDK, but an intermediate cache that ever saw one must not keep
|
|
101
|
+
* it: an invoice is one buyer's address, not a product photo.
|
|
102
|
+
*/
|
|
103
|
+
metadata: { cacheControl: "private, no-store" },
|
|
104
|
+
resumable: false,
|
|
105
|
+
});
|
|
106
|
+
/*
|
|
107
|
+
* 🛑 The order is updated AFTER the bytes exist. The reverse ordering would
|
|
108
|
+
* leave an order claiming an `invoiceStoragePath` that resolves to nothing,
|
|
109
|
+
* and the serve route would then 404 on a document the buyer is entitled to
|
|
110
|
+
* — while the number is already consumed, so retrying cannot re-issue it.
|
|
111
|
+
*
|
|
112
|
+
* A crash between the save and this update leaves an unreferenced object
|
|
113
|
+
* (a few KB) and an order with a number but no path, which the next trigger
|
|
114
|
+
* re-renders into a NEW object. Wasteful and harmless, which is the right
|
|
115
|
+
* side of this trade.
|
|
116
|
+
*/
|
|
117
|
+
await orderRepository.update(order.id, {
|
|
118
|
+
invoiceNumber: allocation.number,
|
|
119
|
+
invoiceIssuedAt: issuedAt,
|
|
120
|
+
invoiceStoragePath: storagePath,
|
|
121
|
+
});
|
|
122
|
+
serverLogger.info("[invoicing] invoice issued", {
|
|
123
|
+
orderId: order.id,
|
|
124
|
+
invoiceNumber: allocation.number,
|
|
125
|
+
});
|
|
126
|
+
return {
|
|
127
|
+
invoiceNumber: allocation.number,
|
|
128
|
+
invoiceStoragePath: storagePath,
|
|
129
|
+
issued: allocation.allocated,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Persist bytes rendered on demand, for an order that is already NUMBERED.
|
|
134
|
+
*
|
|
135
|
+
* ## Why the lazy path exists at all
|
|
136
|
+
*
|
|
137
|
+
* Numbering happens in `onOrderStatusChange`, which runs in appkit and cannot
|
|
138
|
+
* render — `pdfkit` is a Functions dependency and bringing it into appkit's
|
|
139
|
+
* module graph would add it to every consumer build (Rule #6's build weight,
|
|
140
|
+
* Root Cause #18's import chain). So the two halves are split: the number is
|
|
141
|
+
* allocated immediately and transactionally, the bytes are produced the first
|
|
142
|
+
* time anyone asks for them and then stored forever.
|
|
143
|
+
*
|
|
144
|
+
* That split is a deliberate trade and worth naming: the first download of a
|
|
145
|
+
* given invoice pays a render, and every later one does not. The alternative —
|
|
146
|
+
* a second Firestore trigger on `orders` living in `functions/` — is the shape
|
|
147
|
+
* that burned a million invocations a day in Root Cause #92.
|
|
148
|
+
*
|
|
149
|
+
* 🛑 Refuses to overwrite an existing path. A second numbered copy of one
|
|
150
|
+
* invoice is two documents claiming one number, and the one a buyer already
|
|
151
|
+
* downloaded is the one that counts.
|
|
152
|
+
*
|
|
153
|
+
* 🛑 Refuses an un-numbered order. Storing a PDF before the number exists
|
|
154
|
+
* would freeze a pre-payment snapshot as "the invoice", and the real issuance
|
|
155
|
+
* would then find a path already set.
|
|
156
|
+
*/
|
|
157
|
+
export async function storeRenderedInvoice(orderId, bytes) {
|
|
158
|
+
const order = await orderRepository.findById(orderId);
|
|
159
|
+
if (!order?.invoiceNumber)
|
|
160
|
+
return null;
|
|
161
|
+
if (order.invoiceStoragePath)
|
|
162
|
+
return order.invoiceStoragePath;
|
|
163
|
+
const storagePath = `${INVOICE_PREFIX_PATH}/${orderId}/${randomUUID()}.pdf`;
|
|
164
|
+
await getAdminStorage()
|
|
165
|
+
.bucket()
|
|
166
|
+
.file(storagePath)
|
|
167
|
+
.save(bytes, {
|
|
168
|
+
contentType: "application/pdf",
|
|
169
|
+
metadata: { cacheControl: "private, no-store" },
|
|
170
|
+
resumable: false,
|
|
171
|
+
});
|
|
172
|
+
// Path written only after the bytes exist — the reverse would point at
|
|
173
|
+
// nothing and 404 a document the buyer is entitled to.
|
|
174
|
+
await orderRepository.update(orderId, { invoiceStoragePath: storagePath });
|
|
175
|
+
serverLogger.info("[invoicing] invoice bytes stored on first download", {
|
|
176
|
+
orderId,
|
|
177
|
+
invoiceNumber: order.invoiceNumber,
|
|
178
|
+
});
|
|
179
|
+
return storagePath;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* The stored bytes for an already-issued invoice, or null.
|
|
183
|
+
*
|
|
184
|
+
* Returns null rather than throwing on a missing object, because the caller's
|
|
185
|
+
* correct response is to fall back to an on-demand render for a pre-Phase-7
|
|
186
|
+
* order — and a throw would turn a legacy order's invoice page into an error.
|
|
187
|
+
*/
|
|
188
|
+
export async function readStoredInvoice(storagePath) {
|
|
189
|
+
if (!storagePath)
|
|
190
|
+
return null;
|
|
191
|
+
try {
|
|
192
|
+
const [buffer] = await getAdminStorage().bucket().file(storagePath).download();
|
|
193
|
+
return buffer;
|
|
194
|
+
}
|
|
195
|
+
catch (err) {
|
|
196
|
+
serverLogger.warn("[invoicing] stored invoice unreadable — falling back to render", {
|
|
197
|
+
storagePath,
|
|
198
|
+
error: normalizeError(err).message,
|
|
199
|
+
});
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type ActionResult } from "../../../shared/types/action-result";
|
|
2
|
+
import { type DisputeKind } from "../../../shared/features/orders/dispute-reasons";
|
|
2
3
|
export declare function createOrderAction(input: unknown): Promise<ActionResult<unknown>>;
|
|
3
4
|
/**
|
|
4
5
|
* Cancel an order, and give the money back if any was taken (D14).
|
|
@@ -58,7 +59,13 @@ export declare function adminRejectPaymentAsFraudAction(orderId: string, note: s
|
|
|
58
59
|
* the seller (store owner), or admin/moderator. Does not itself reverse
|
|
59
60
|
* payment/order status — surfaces to the admin queue for investigation.
|
|
60
61
|
*/
|
|
61
|
-
export declare function raiseOrderDisputeAction(orderId: string,
|
|
62
|
+
export declare function raiseOrderDisputeAction(orderId: string, input: {
|
|
63
|
+
kind: DisputeKind;
|
|
64
|
+
/** `disputeReasons/{id}` — must exist and must match `kind`. */
|
|
65
|
+
reasonId: string;
|
|
66
|
+
/** The buyer's own words. Optional; the reason is the required part. */
|
|
67
|
+
detail?: string;
|
|
68
|
+
}): Promise<ActionResult<void>>;
|
|
62
69
|
/**
|
|
63
70
|
* A buyer submits payment for ONE of their own EMI instalments.
|
|
64
71
|
*
|