feat: Implement free issue tracking and scheme discount handling in invoices and Shop POs
This commit is contained in:
@@ -76,7 +76,7 @@ order must contain every component in at least its listed `Qty` before the schem
|
||||
SchemeRedemption
|
||||
├── Id (Guid) — PK
|
||||
├── FK_SchemeId (Guid) — required
|
||||
├── FK_OrderId (Guid?) — nullable (see FieldInvoiceId below)
|
||||
├── FK_ShopPOId (Guid?) — nullable (see FieldInvoiceId below); the Shop PO the scheme was applied to
|
||||
├── FieldInvoiceId (Guid?) — placeholder for the future field-invoice module; no FieldInvoice
|
||||
│ entity exists yet, so this is an unconstrained Guid for now
|
||||
├── DiscountAmount (decimal?) — set for SlabDiscount/FlatPercent redemptions
|
||||
@@ -87,8 +87,8 @@ SchemeRedemption
|
||||
│ later changes
|
||||
├── RedeemedAt (DateTime)
|
||||
```
|
||||
At least one of `DiscountAmount` or `FreeQty` is set per redemption; `Scheme`/`Order` restrict-delete
|
||||
(a scheme or order with redemptions can't be hard-deleted, and soft-delete leaves history intact).
|
||||
At least one of `DiscountAmount` or `FreeQty` is set per redemption; `Scheme`/`ShopPO` restrict-delete
|
||||
(a scheme or Shop PO with redemptions can't be hard-deleted, and soft-delete leaves history intact).
|
||||
|
||||
### Item (extended)
|
||||
```
|
||||
@@ -124,8 +124,10 @@ columns/child table after the first migration had already been applied to the de
|
||||
| — | `DELETE /api/schemes/{id}/free-goods-rule` | Clears it |
|
||||
| — | `PUT /api/schemes/{id}/bundle-components/{itemId}` | Upserts one FreeBundle required-item row |
|
||||
| — | `DELETE /api/schemes/{id}/bundle-components/{itemId}` | Removes one FreeBundle required-item row |
|
||||
| `evaluateSchemeEligibility(storeId, orderId)` | `GET /api/schemes/eligible?storeId=...&orderId=...` | Returns eligibility for every in-scope scheme, not just one |
|
||||
| — | `POST /api/schemes/suggest` | Body `{ storeId, items:[{itemId, quantity, unitPrice?}] }`; scheme suggestions for a Shop PO that is not saved yet |
|
||||
| `evaluateSchemeEligibility(storeId, shopPOId)` | `GET /api/schemes/eligible?storeId=...&orderId=...` (`orderId` is the Shop PO id) | Returns eligibility for every in-scope scheme, not just one |
|
||||
| `applySchemeDiscount(orderId, schemeId)` | `POST /api/orders/{id}/apply-scheme` | Adds a zero-price order line for free-goods payouts |
|
||||
| — | `POST /api/invoices/{id}/preview-totals` | Draft-editor preview of discount, total and free entitlement for unsaved lines |
|
||||
| `trackSchemeSpend(schemeId)` | `GET /api/schemes/{id}/spend` | Discount spend + free-goods cost spend |
|
||||
| `getSchemeROI(schemeId)` | `GET /api/schemes/{id}/roi` | Uses `BaselineExpectedSales` |
|
||||
|
||||
@@ -216,13 +218,18 @@ Use this to add/edit one bundle component after creation.
|
||||
[08_sales_agent_controls.md](./08_sales_agent_controls.md) unless `AgentLimitExempt = true`.
|
||||
(Enforcement against the actual ceiling is deferred — 08_sales_agent_controls.md's discount
|
||||
ceiling isn't implemented in code yet; this is recorded as the target behavior.)
|
||||
2. `evaluateSchemeEligibility` matches a scheme to a store/order only if: the scheme's `RegionId` is
|
||||
2. `evaluateSchemeEligibility` matches a scheme to a store/Shop PO only if: the scheme's `RegionId` is
|
||||
null or matches the store's region; `EligibleSegment` is null or matches `Store.Segment`; and the
|
||||
order's date falls within `[DateRangeFrom, DateRangeTo]`.
|
||||
3. `BuyXGetY` eligibility: the order must contain at least `TriggerQty` of `TriggerItem`; the payout
|
||||
Shop PO's date falls within `[DateRangeFrom, DateRangeTo]`. `DateRangeTo` is inclusive of the
|
||||
whole calendar day (compared by date, so a scheme ending `2026-08-31` still applies on the 31st).
|
||||
3. `BuyXGetY` eligibility: the Shop PO must contain at least `TriggerQty` of `TriggerItem`; the payout
|
||||
scales with how many multiples of `TriggerQty` were bought (`FreeQty × floor(boughtQty / TriggerQty)`).
|
||||
4. `FreeBundle` eligibility: the order must contain every `SchemeBundleComponent` in at least its
|
||||
listed `Qty`; payout is the flat `FreeQty` of `FreeItem` (no multiplier).
|
||||
Only **paid** lines (`UnitPrice > 0`) count toward `boughtQty` — free lines added by an earlier
|
||||
redemption never count toward a trigger, so "buy 10 get 1 of the same item" can't self-inflate.
|
||||
4. `FreeBundle` eligibility: the Shop PO must contain every `SchemeBundleComponent` in at least its
|
||||
listed `Qty` (paid lines only, same as rule 3); payout is the flat `FreeQty` of `FreeItem` (no multiplier).
|
||||
`FreeQty` must be a whole number (validated on create / `PUT .../free-goods-rule`), because
|
||||
Shop PO lines hold whole units.
|
||||
5. `applySchemeDiscount` adds a zero-price `OrderItem` line for the free-goods payout (`UnitPrice = 0`,
|
||||
`Quantity = FreeQty`), rather than only recording a discount amount — the free line still needs to
|
||||
be picked, delivered, and stock-tracked like any other sold line.
|
||||
@@ -240,6 +247,80 @@ Use this to add/edit one bundle component after creation.
|
||||
|
||||
---
|
||||
|
||||
## Offers on the PO and Invoice (implemented)
|
||||
|
||||
Nothing is added silently: the system **suggests**, the user **accepts**, and what was accepted stays **editable**.
|
||||
|
||||
### 1. On the Shop PO (new and edit screens)
|
||||
1. While paid lines are entered, the screen calls `POST /api/schemes/suggest` (`{ storeId, items:[{itemId, quantity, unitPrice?}] }`).
|
||||
The server returns the in-scope schemes (region, segment, date) the lines qualify for, plus Buy & get free /
|
||||
Combo gift schemes the cart is part-way to (`reason` doubles as a nudge, e.g. "Requires 10 of the trigger item;
|
||||
ShopPO has 6").
|
||||
2. The **Offers & Free Issues** panel shows:
|
||||
- **Free goods** (BuyXGetY / FreeBundle): "Qualifies for N free X" with **Add free**. The accepted free line is
|
||||
editable (qty, remove) and is saved as a line with `isFree = true` and the `schemeId`.
|
||||
- **Percentage discount** (FlatPercent / SlabDiscount): "Saves Rs. X" with **Apply discount**. Only one discount
|
||||
per order. The accepted scheme ids are sent as `discountSchemeIds` on create / update-items.
|
||||
3. If a later paid-qty change shrinks what the order earns, the screen trims the free qty / drops the discount and
|
||||
says so. The server enforces the same rules on save: free qty within the entitlement, free item in stock, scheme
|
||||
active and in scope, at most one discount, order still qualifies.
|
||||
4. Saving keeps `SchemeRedemption` in step with the lines: created or resized per free line, removed with its line,
|
||||
and discount redemptions created, recomputed, or dropped if the order stops qualifying.
|
||||
5. `GET /api/shop-pos/{id}` returns `discounts[]` (scheme, amount) so the edit screen starts with the right state and
|
||||
the PO view shows a "Scheme discount" row above the total. `PO.TotalAmount` stays the full lines total.
|
||||
|
||||
### 2. On the invoice
|
||||
- The invoice copies the PO lines, including free lines (`IsFree`, `FK_SchemeId`, `SchemeName`). Free lines show in a
|
||||
**Free Issue** column (a "-" in the cell that does not apply), are listed **after all paid lines**, are zero-priced
|
||||
(price cannot be edited) and can be lowered like any line while the invoice is Draft.
|
||||
- `Invoice.TotalAmount = billable lines - DiscountAmount`. When the invoice is created from a PO it starts at the
|
||||
discounted total.
|
||||
- **Draft invoice editor** (`/sales/invoices/{id}/edit`):
|
||||
- A **Scheme discount** box lists the flat / slab discounts the current lines qualify for with **Apply discount** /
|
||||
**Remove**. This also covers a PO that was confirmed before a discount was applied. `PUT /api/invoices/{id}/items`
|
||||
accepts `discountSchemeIds` (null = leave as is).
|
||||
- `POST /api/invoices/{id}/preview-totals` (`{ items:[{itemId, quantity, unitPrice, isFree, isRemoved, schemeId}], discountSchemeIds? }`)
|
||||
returns the discount, total and per-scheme free entitlement for the unsaved lines; the editor calls it (debounced)
|
||||
so the Scheme discount row and Total update while typing. It changes nothing.
|
||||
- **Recalculation.** On every line change (add, remove, restore, re-price, quantity) and again at print and finalize,
|
||||
`InvoiceService.ApplyTotalsAsync` first reconciles free lines, then re-evaluates the discount against the current lines:
|
||||
- FlatPercent stays the set % of the current lines total; SlabDiscount can move up or down a level, or drop to 0 and
|
||||
return if the lines requalify. Free lines and struck-out lines are ignored. The redemption's `DiscountAmount` is
|
||||
updated with it, so scheme spend / ROI stay correct.
|
||||
- **Free lines only shrink automatically.** If the paid quantity that earned a free issue is lowered or removed, the free
|
||||
quantity is reduced to what is still earned (a line that earns nothing is struck out, keeping its quantity so it can be
|
||||
restored) and the PO's free-goods redemption is resized. Free quantity is never raised automatically; a person raises it,
|
||||
up to what the scheme earns.
|
||||
- The invoice page's Items table ends with **Subtotal / Scheme discount / Total**, and the printed A4 and receipt copies show
|
||||
Subtotal, Scheme discount, Total (Paid / Balance Due use the discounted total).
|
||||
- The older `POST /api/orders/{id}/apply-scheme` endpoint still works for an already-saved PO and flags the line the same way
|
||||
(and mirrors a free line onto a still-Draft invoice).
|
||||
|
||||
### 3. Scheme setup screens
|
||||
`/pricing/schemes/new`, `/pricing/schemes/{id}` (read-only) and `/pricing/schemes/{id}/edit` are full pages (no modal) built on one
|
||||
form. Rule types are shown in plain words: **Buy & get free** (BuyXGetY), **Combo gift** (FreeBundle), **Flat discount**
|
||||
(FlatPercent), **Spend more, save more** (SlabDiscount); the fields adapt to the type (product dropdowns, quantities, % and
|
||||
levels) with a live summary sentence. Required fields come first; Active, then the optional Region / Store segment last. The scheme
|
||||
name is optional (generated from the offer when blank). The rule type cannot change after creation.
|
||||
|
||||
### Data
|
||||
`ShopPOItem` and `InvoiceItem` gained `IsFree`, `FK_SchemeId` (plain Guid, no FK constraint) and a `SchemeName` snapshot. Migration
|
||||
`AddFreeIssueLines` also back-fills existing zero-priced redemption lines.
|
||||
|
||||
### Not done yet
|
||||
`SchemeRedemption.FK_InvoiceId` (`FieldInvoiceId` is still an unused placeholder); invoice-version snapshots do not record `IsFree`;
|
||||
no hint or one-click "add more free units" when the paid quantity rises; scope (date / region) is checked when a discount is first
|
||||
applied, not re-checked on later invoice edits; Slab "close to the next level" hints are not shown.
|
||||
|
||||
## Known Gaps vs. Enterprise Practice
|
||||
|
||||
Scheme code, lifecycle (Draft/Approved/Active/Expired), priority and stackable/exclusive flags;
|
||||
budget and per-store caps; triggers on product groups/brands/categories or order value; tiered
|
||||
free-goods slabs; funding source (company vs supplier — ties into SupplierClaims and GRN
|
||||
`FreeQuantity`); stock reservation for the free item (the current check only sums balances).
|
||||
|
||||
---
|
||||
|
||||
## Open Questions
|
||||
|
||||
- What counts as "incremental sales" for `getSchemeROI` — **resolved for v1**: a manual
|
||||
|
||||
@@ -95,6 +95,7 @@ InvoiceItem — the live billing lines
|
||||
├── FK_InvoiceId, FK_ShopPOItemId, FK_ItemId
|
||||
├── Quantity (int) — what is billed now (0..ShopPOItem.Quantity)
|
||||
├── UnitPrice (decimal)
|
||||
├── IsFree, FK_SchemeId, SchemeName — free issue copied from the PO line (UnitPrice 0); see 17_promotions_schemes.md
|
||||
└── IsActive, CreatedAt — from Template
|
||||
|
||||
InvoiceVersion — immutable snapshot of the invoice
|
||||
@@ -145,7 +146,8 @@ invoice for the preview, the draft editor and the print tab:
|
||||
`*** PRINTED ***`, `qty x price` lines, heavy-ruled TOTAL, Paid/Balance, payments, "Thank you!").
|
||||
- Money `Rs. 12,450.00`, quantities up to 3 decimals, dates `dd/MM/yyyy`.
|
||||
- Deleted lines are shown **struck through in every status** and excluded from all totals.
|
||||
- Not included (no data in the app): company/"From" block, logo, bank details, discount, tax, due date.
|
||||
- Scheme discount: shown as Subtotal / Scheme discount / Total (`Invoice.DiscountAmount`), re-derived from the current lines on every edit, print and finalize (see 17_promotions_schemes.md). Free-issue lines get a **Free Issue** column and come after the paid lines.
|
||||
- Not included (no data in the app): company/"From" block, logo, bank details, tax, due date.
|
||||
|
||||
**Print tab** — `/invoices/{id}/print` (own route group `(print)`, no dashboard chrome). Has a
|
||||
paper-size selector (A4 / 80mm / 58mm), sets `@page` with `margin: 0` (so the browser's own
|
||||
@@ -290,8 +292,9 @@ Draft → Printed → Finalized
|
||||
also unresolved. Both cases need explicit cascade rules before implementation.
|
||||
6. **Exact adjustment payload for `POST /finalize`** — *partly resolved:* quantity adjustments
|
||||
(`{ shopPOItemId, adjustedQuantity }`) are implemented and persisted on the invoice lines.
|
||||
Returns, damage and discounts are still open, as is the payload shape for them. "Free issues" handling (items
|
||||
given to the shop at no charge) is a known case that needs its own field(s) in the payload.
|
||||
Returns and damage are still open, as is the payload shape for them. Scheme discounts and free issues are now handled
|
||||
by the invoice itself (re-evaluated on every edit, print and finalize; free lines only ever shrink automatically) — see
|
||||
[17_promotions_schemes.md](../17_promotions_schemes.md).
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -34,9 +34,11 @@ const statusLabel = (invoice: InvoiceResponse) =>
|
||||
|
||||
// Removed lines stay on the document (struck through) but are excluded from every total.
|
||||
function totals(invoice: InvoiceResponse, payments: PaymentResponse[]) {
|
||||
const total = invoice.items.filter((i) => !i.isRemoved).reduce((s, i) => s + i.quantity * i.unitPrice, 0)
|
||||
const subtotal = invoice.items.filter((i) => !i.isRemoved).reduce((s, i) => s + i.quantity * i.unitPrice, 0)
|
||||
const discount = Math.min(invoice.discountAmount ?? 0, subtotal)
|
||||
const total = subtotal - discount
|
||||
const paid = payments.reduce((s, p) => s + p.amount, 0)
|
||||
return { total, paid, balance: total - paid }
|
||||
return { subtotal, discount, total, paid, balance: total - paid }
|
||||
}
|
||||
|
||||
export function InvoiceTemplate({ paper = 'A4', ...props }: InvoiceTemplateProps) {
|
||||
@@ -56,9 +58,12 @@ function A4Sheet({
|
||||
showAgentEdits,
|
||||
className,
|
||||
}: Omit<InvoiceTemplateProps, 'paper'>) {
|
||||
const { total, paid, balance } = totals(invoice, payments)
|
||||
const { subtotal, discount, total, paid, balance } = totals(invoice, payments)
|
||||
const status = statusLabel(invoice)
|
||||
const hasRemoved = invoice.items.some((i) => i.isRemoved)
|
||||
const hasFree = invoice.items.some((i) => i.isFree)
|
||||
// Free issues are listed after every paid line.
|
||||
const rows = [...invoice.items].sort((a, b) => Number(a.isFree) - Number(b.isFree))
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -107,6 +112,7 @@ function A4Sheet({
|
||||
<tr className="border-b border-neutral-300 text-xs uppercase tracking-wide text-neutral-500">
|
||||
<th className="px-2 py-2 text-left font-medium">Item</th>
|
||||
<th className="w-24 px-2 py-2 text-right font-medium">Qty</th>
|
||||
{hasFree && <th className="w-24 px-2 py-2 text-right font-medium">Free Issue</th>}
|
||||
<th className="w-36 px-2 py-2 text-right font-medium">Unit Price</th>
|
||||
<th className="w-40 px-2 py-2 text-right font-medium">Amount</th>
|
||||
{renderActions && <th className="w-24 px-2 py-2 print:hidden" />}
|
||||
@@ -118,15 +124,25 @@ function A4Sheet({
|
||||
<td colSpan={4} className="px-2 py-6 text-center text-neutral-500">No items.</td>
|
||||
</tr>
|
||||
) : (
|
||||
invoice.items.map((item) => (
|
||||
rows.map((item) => (
|
||||
<tr
|
||||
key={item.id}
|
||||
className={cn('break-inside-avoid border-b border-neutral-200', item.isRemoved && 'text-neutral-400 line-through')}
|
||||
>
|
||||
<td className="px-2 py-2.5">{item.itemName}</td>
|
||||
<td className="px-2 py-2.5 text-right">
|
||||
{renderQuantity && !item.isRemoved ? renderQuantity(item) : fmtQty(item.quantity)}
|
||||
<td className="px-2 py-2.5">
|
||||
{item.itemName}
|
||||
{item.isFree && item.schemeName && (
|
||||
<span className="block text-xs text-neutral-500 no-underline">Free issue · {item.schemeName}</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-2 py-2.5 text-right">
|
||||
{item.isFree ? '-' : renderQuantity && !item.isRemoved ? renderQuantity(item) : fmtQty(item.quantity)}
|
||||
</td>
|
||||
{hasFree && (
|
||||
<td className="px-2 py-2.5 text-right">
|
||||
{item.isFree ? (renderQuantity && !item.isRemoved ? renderQuantity(item) : fmtQty(item.quantity)) : '-'}
|
||||
</td>
|
||||
)}
|
||||
<td className="px-2 py-2.5 text-right">
|
||||
{renderUnitPrice && !item.isRemoved ? renderUnitPrice(item) : fmtMoney(item.unitPrice)}
|
||||
</td>
|
||||
@@ -160,8 +176,14 @@ function A4Sheet({
|
||||
<dl className="w-72 break-inside-avoid text-sm">
|
||||
<div className="flex justify-between border-b py-1.5">
|
||||
<dt className="text-neutral-500">Subtotal</dt>
|
||||
<dd>{fmtMoney(total)}</dd>
|
||||
<dd>{fmtMoney(subtotal)}</dd>
|
||||
</div>
|
||||
{discount > 0 && (
|
||||
<div className="flex justify-between border-b py-1.5">
|
||||
<dt className="text-neutral-500">Scheme discount</dt>
|
||||
<dd>-{fmtMoney(discount)}</dd>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex justify-between border-y py-2 text-base font-bold" style={{ borderColor: RULE }}>
|
||||
<dt>Total</dt>
|
||||
<dd>{fmtMoney(total)}</dd>
|
||||
@@ -225,7 +247,7 @@ function ReceiptSheet({
|
||||
showAgentEdits,
|
||||
className,
|
||||
}: Omit<InvoiceTemplateProps, 'paper'> & { paper: Exclude<InvoicePaper, 'A4'> }) {
|
||||
const { total, paid, balance } = totals(invoice, payments)
|
||||
const { subtotal, discount, total, paid, balance } = totals(invoice, payments)
|
||||
const status = statusLabel(invoice)
|
||||
const narrow = paper === '58mm'
|
||||
const rule = <div className="my-2 border-t border-dashed border-black" />
|
||||
@@ -252,12 +274,21 @@ function ReceiptSheet({
|
||||
</div>
|
||||
{rule}
|
||||
<div className="space-y-1.5">
|
||||
{invoice.items.map((i) => (
|
||||
{[...invoice.items].sort((a, b) => Number(a.isFree) - Number(b.isFree)).map((i) => (
|
||||
<div key={i.id} className={cn(i.isRemoved && 'line-through opacity-60')}>
|
||||
<p>{i.itemName}</p>
|
||||
<p className="flex justify-between">
|
||||
<span>{fmtQty(i.quantity)} x {num(i.unitPrice)}</span>
|
||||
<span>{num(i.quantity * i.unitPrice)}</span>
|
||||
{i.isFree ? (
|
||||
<>
|
||||
<span>{fmtQty(i.quantity)} FREE{i.schemeName ? ` (${i.schemeName})` : ''}</span>
|
||||
<span>{num(0)}</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span>{fmtQty(i.quantity)} x {num(i.unitPrice)}</span>
|
||||
<span>{num(i.quantity * i.unitPrice)}</span>
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
@@ -265,8 +296,14 @@ function ReceiptSheet({
|
||||
{rule}
|
||||
<p className="flex justify-between">
|
||||
<span>Subtotal</span>
|
||||
<span>{fmtMoney(total)}</span>
|
||||
<span>{fmtMoney(subtotal)}</span>
|
||||
</p>
|
||||
{discount > 0 && (
|
||||
<p className="flex justify-between">
|
||||
<span>Scheme discount</span>
|
||||
<span>-{fmtMoney(discount)}</span>
|
||||
</p>
|
||||
)}
|
||||
<div className="my-1 border-y-2 border-black py-1 text-sm font-bold">
|
||||
<p className="flex justify-between">
|
||||
<span>TOTAL</span>
|
||||
|
||||
@@ -661,6 +661,18 @@ export interface SchemeResponse {
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
export interface SchemeSuggestion {
|
||||
schemeId: string
|
||||
schemeName: string
|
||||
ruleType: SchemeRuleType
|
||||
eligible: boolean
|
||||
reason: string | null
|
||||
discountAmount: number | null
|
||||
freeItemId: string | null
|
||||
freeQty: number | null
|
||||
freeItemName: string | null
|
||||
}
|
||||
|
||||
// ─── Suppliers ────────────────────────────────────────────────────────────────
|
||||
|
||||
export interface SupplierResponse {
|
||||
@@ -973,6 +985,15 @@ export interface ShopPOItemResponse {
|
||||
quantityFulfilled: number
|
||||
unitPrice: number
|
||||
isActive: boolean
|
||||
isFree: boolean
|
||||
schemeId: string | null
|
||||
schemeName: string | null
|
||||
}
|
||||
|
||||
export interface ShopPODiscount {
|
||||
schemeId: string
|
||||
schemeName: string
|
||||
amount: number
|
||||
}
|
||||
|
||||
export interface ShopPOResponse {
|
||||
@@ -1000,6 +1021,7 @@ export interface ShopPOResponse {
|
||||
cancelReason: string | null
|
||||
cancelledByName: string | null
|
||||
cancelledByRole: string | null
|
||||
discounts: ShopPODiscount[]
|
||||
}
|
||||
|
||||
export interface InvoiceItemResponse {
|
||||
@@ -1011,6 +1033,9 @@ export interface InvoiceItemResponse {
|
||||
quantity: number
|
||||
unitPrice: number
|
||||
isRemoved: boolean
|
||||
isFree: boolean
|
||||
schemeId: string | null
|
||||
schemeName: string | null
|
||||
}
|
||||
|
||||
export interface InvoiceVersionItemResponse {
|
||||
@@ -1050,6 +1075,7 @@ export interface InvoiceResponse {
|
||||
isActive: boolean
|
||||
createdAt: string
|
||||
items: InvoiceItemResponse[]
|
||||
discounts: ShopPODiscount[]
|
||||
}
|
||||
|
||||
export interface OutstandingPaymentsResponse {
|
||||
|
||||
Reference in New Issue
Block a user