Supercommerce API Docs
Full Module Docs

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 via TaxModule.forRoot({ providers }) in apps/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). See order.md for the response shape.


Conventions

Authentication

Endpoint groupAuthPermission
GET /vendor/tax/config, PATCH /vendor/tax/configrequired (vendor session)active vendor in session
GET /admin/vendors/:vendorId/tax/configrequiredplatformVendorSetting: read
PATCH /admin/vendors/:vendorId/tax/configrequiredplatformVendorSetting: 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

statusCodeerrorCode examples
400BAD_REQUEST, VALIDATION_ERROR
401UNAUTHORIZED
403FORBIDDEN
500INTERNAL_SERVER_ERROR, DATABASE_ERROR

Currency / numbers

  • rate is a percentage in [0, 100] (e.g. 9 for 9%, 18 for 18%).
  • amount (on TaxComponent) is in integer subunits (paise / cents) — same convention as every other money field in the platform (subunit pricing memory).

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:

InputOutput
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:

BodyEffect
{} (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 }
  ]
}
FieldConstraints
taxes[].type1..50 chars, trimmed
taxes[].ratenumber, 0..100

Response 200 — updated FlatTaxConfigResponse.

Errors

StatusCodeWhen
400VALIDATION_ERRORBody fails zod (rate out of range, empty type)
403FORBIDDENNo 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 200FlatTaxConfigResponse.


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

StatusCodeWhen
400VALIDATION_ERRORBody fails zod
403FORBIDDENCaller 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):

  1. Implement TaxProvider in a new module.
  2. Register it via the base module's forRoot({ providers: [...] }).
  3. Expose its own vendor + admin config endpoints (mirror tax-flat).
  4. Add the provider id to the vendor's admin.tax.provider setting key — the registry routes computations through the chosen provider per vendor.

Today only "flat" is registered.


  • order — surfaces tax_breakdown + netAmount on lines and the sub-order rollup. See order.md.
  • cart — cart-side preview applies the same provider for the cart breakdown. See cart.md.
  • settings — underlying storage for vendor tax config; the focused endpoints in this module are a typed view over vendor.admin.tax.* keys. See settings.md.
  • admin-rbac — the platformVendorSetting:* permission gates the admin override surface. See admin-rbac.md.

On this page