Tax Module
Two-part tax system. Base tax module defines the neutral TaxProvider port + registry and shared types (TaxConfigLine, TaxComponent). tax-flat provider is the concrete…
Two-part tax system. Base tax module defines the neutral TaxProvider port + registry and shared types (TaxConfigLine, TaxComponent). tax-flat provider is the concrete implementation today — vendor declares a fixed list of (type, rate%) rows; the cart/order applies all of them to each taxable amount.
Sources:
api-modules/tax— port, registry, types. No HTTP endpoints. Registered viaTaxModule.forRoot({ providers })inapps/api/src/app.module.ts.api-modules/tax-flat— flat provider (id"flat"). HTTP surface for vendor self-service and platform-admin override.Tax computations land on order lines (
order_line.tax_breakdown,netAmount) and on the per-vendor sub-order rollup (order_vendor.tax_breakdown,shippingTaxBreakdown). Seeorder.mdfor the response shape.
Conventions
Authentication
| Endpoint group | Auth | Permission |
|---|---|---|
GET /vendor/tax/config, PATCH /vendor/tax/config | required (vendor session) | active vendor in session |
GET /admin/vendors/:vendorId/tax/config | required | platformVendorSetting: read |
PATCH /admin/vendors/:vendorId/tax/config | required | platformVendorSetting: update |
The vendor self-service surface targets the vendor in the session (resolveActiveVendorId); the platform-admin override surface takes the target vendor id from the URL. Both eventually write through VendorSettingsService (audited per key).
Response envelope
Successful responses are wrapped by ResponseInterceptor:
{
"data": <payload>,
"message": "Success",
"statusCode": 200,
"metadata": null
}Error envelope
statusCode | errorCode examples |
|---|---|
| 400 | BAD_REQUEST, VALIDATION_ERROR |
| 401 | UNAUTHORIZED |
| 403 | FORBIDDEN |
| 500 | INTERNAL_SERVER_ERROR, DATABASE_ERROR |
Currency / numbers
rateis a percentage in[0, 100](e.g.9for 9%,18for 18%).amount(onTaxComponent) is in integer subunits (paise / cents) — same convention as every other money field in the platform (subunit pricingmemory).
Domain types
TaxConfigLine
One row in a vendor's tax-config UI. Used by the flat provider's vendor + admin endpoints. Lives in the base tax module so other providers can reuse the validation when they accept similar input.
type TaxConfigLine = {
type: string; // 1..50 chars; e.g. "CGST", "SGST", "VAT"
rate: number; // percentage, 0..100
};TaxComponent
One row in a computed breakdown — what the cart/order returns to clients.
type TaxComponent = {
type: string; // e.g. "CGST"
rate: number; // percentage, 0..100
amount: number; // subunits applied to this line / shipping row
};FlatTaxConfigResponse
The vendor's full tax-flat config — a list of TaxConfigLine rows.
type FlatTaxConfigResponse = {
taxes: TaxConfigLine[];
};Tax model
Inclusive vs net
Prices on the platform are tax-inclusive (unit_price, line_subtotal, shipping_total already include applicable taxes). The tax module computes the pre-tax netAmount and the per-component amount for each line + shipping row by reverse-applying the configured rates:
totalRate = sum(rate for row in taxes) / 100 // e.g. 0.18
netAmount = lineSubtotal / (1 + totalRate)
component[i] = netAmount * (taxes[i].rate / 100)The output is what surfaces on order_line.tax_breakdown (and the analogous shipping_tax_breakdown for shipping). netAmount and taxBreakdown are both null / [] when the vendor has no taxes configured (taxes: []).
Tax-flat provider
The provider id is "flat". Behavior:
| Input | Output |
|---|---|
taxes: [] | No tax — netAmount = null, taxBreakdown = [] everywhere |
taxes: [{ type: "CGST", rate: 9 }, { type: "SGST", rate: 9 }] | 9% + 9% = 18%. For a ₹1,180 line (118000 subunits): netAmount = 100000, taxBreakdown = [{ CGST, 9, 9000 }, { SGST, 9, 9000 }] |
There is no row-level conditional (no "only items in category X" yet) — every configured row applies to every taxable amount on the vendor.
Vendor — self-service
Base path: /vendor/tax. Auth: vendor session with active vendor.
GET /vendor/tax/config — Current tax rows
Response 200
{
"data": {
"taxes": [
{ "type": "CGST", "rate": 9 },
{ "type": "SGST", "rate": 9 }
]
},
"message": "Success",
"statusCode": 200
}Empty array (taxes: []) means the vendor has no taxes configured — orders include no tax breakdown.
PATCH /vendor/tax/config — Replace tax rows
Replaces the whole list. Two distinct semantics:
| Body | Effect |
|---|---|
{} (omit taxes) | List untouched; no-op |
{ "taxes": [] } | Explicitly clears the list — no tax applied to future orders |
{ "taxes": [...] } | Replaces the list with the new rows |
Body
{
"taxes": [
{ "type": "CGST", "rate": 9 },
{ "type": "SGST", "rate": 9 }
]
}| Field | Constraints |
|---|---|
taxes[].type | 1..50 chars, trimmed |
taxes[].rate | number, 0..100 |
Response 200 — updated FlatTaxConfigResponse.
Errors
| Status | Code | When |
|---|---|---|
| 400 | VALIDATION_ERROR | Body fails zod (rate out of range, empty type) |
| 403 | FORBIDDEN | No active vendor on session |
Side effects — audit row per changed key via VendorSettingsService. The config lives in the vendor admin.tax settings group (one key per row; the controller is the focused UI over that generic store).
Admin — override any vendor
Base path: /admin/vendors/:vendorId/tax. Required permission: platformVendorSetting:read / :update.
GET /admin/vendors/:vendorId/tax/config — Read
Response 200 — FlatTaxConfigResponse.
PATCH /admin/vendors/:vendorId/tax/config — Override
Required permission: platformVendorSetting: update. Same body shape as the vendor self-service endpoint; bypasses any forAdmin write-guard on the underlying vendor-settings store.
Errors
| Status | Code | When |
|---|---|---|
| 400 | VALIDATION_ERROR | Body fails zod |
| 403 | FORBIDDEN | Caller lacks platformVendorSetting: update |
Adding a new tax provider
The base tax module's TaxProvider port is the extension point:
// api-modules/tax/src/ports/tax-provider.ts
interface TaxProvider {
readonly id: string;
compute(ctx: TaxComputeContext): Promise<TaxComputeResult>;
}To add (e.g. an external-API provider):
- Implement
TaxProviderin a new module. - Register it via the base module's
forRoot({ providers: [...] }). - Expose its own vendor + admin config endpoints (mirror
tax-flat). - Add the provider id to the vendor's
admin.tax.providersetting key — the registry routes computations through the chosen provider per vendor.
Today only "flat" is registered.
Related modules
order— surfacestax_breakdown+netAmounton lines and the sub-order rollup. Seeorder.md.cart— cart-side preview applies the same provider for the cart breakdown. Seecart.md.settings— underlying storage for vendor tax config; the focused endpoints in this module are a typed view overvendor.admin.tax.*keys. Seesettings.md.admin-rbac— theplatformVendorSetting:*permission gates the admin override surface. Seeadmin-rbac.md.
Shipping Module
Two-layer shipping model. Customer-charge layer: every vendor has a flat per-order shipping rate (with optional free-above threshold) that the cart applies as a single line at…
Vendor Module
HTTP surface for vendor onboarding (a user applies to become a vendor; an admin reviews + approves/rejects) and vendor directory reads (admin lists all approved vendors with team…