npm.io
0.1.1 • Published 21h ago

@spinekit/bd-tax

Licence
SEE LICENSE IN LICENSE
Version
0.1.1
Deps
0
Size
397 kB
Vulns
0
Weekly
0

@spinekit/bd-tax

Bangladesh statutory tax filing — Mushak 6.3 VAT invoices + VDS/TDS withholding certificates.

Part of Spine, Classytic's ERP — the arc-module layer that composes the framework-agnostic @classytic/bd-tax kernel into an @classytic/arc application via createApp({ modules }).

Install

npm install @spinekit/bd-tax @classytic/bd-tax @classytic/arc @classytic/mongokit \
  @classytic/primitives mongoose zod

Kernels and the framework are peer dependencies — the host installs a single copy; this module bundles nothing.

Usage

import { createApp } from '@classytic/arc/factory';
import { createBdTaxModule } from '@spinekit/bd-tax';

const app = await createApp({
  modules: [
    createBdTaxModule({
      connection,          // your mongoose connection
      permissions,         // { view, manage, ... } role gates
      // BYO engine + host seams (extraActions, extraRoutes, bridges) as needed
    }),
  ],
});

The module's bootstrap return is recorded at fastify.arc.modules['bd-tax'] for container-free cross-module wiring.

The one rule: the host projects, the module decides

A host resolves its own order into a buyer and raw items — and stops. Fiscal position (which SRO relief applies), rate-code remapping, VAT calculation, serial allocation, the filing window and form selection are Bangladesh.

// HOST: projection. No tax math.
const salesBridge: SalesSourceBridge = async (orderId) => {
  const order = await orders.get(orderId);
  return {
    buyer: {
      name: order.customer.name,
      countryCode: order.shippingAddress.country,  // a FACT, not "INTERNATIONAL"
    },
    items: order.lines.map((l) => ({
      description: l.name, quantity: l.qty, unitPrice: l.unitPriceMinor,
    })),
    sourceModel: 'Order',
    sourceId: orderId,
  };
};

The module then resolves the fiscal position, remaps rate codes, prices every line, builds the Mushak 6.3, allocates the serial and persists it — idempotently on (sourceModel, sourceId), enforced by a unique index rather than a read-then-write check.

POST /generate does the same from raw items directly, under the issue permission (distinct from file: BD VAT Act §51 makes the legal invoice accompany the goods, so a cashier issues one; nobody wants a cashier filing the monthly return).

If you find yourself calling calculateInvoiceTax or resolveFiscalPosition in your host, stop — that is the module's job, and a second copy of it will drift silently. The one legitimate override is bridges.fiscalPosition, for a deployment that merges its own tax classes into a tax-core TaxResolver.

The monthly return — and why you should not re-host it

GET /accounting/musok/return/:period serves the full statutory return: regime → form (STANDARD_VAT… → Mushak 9.1, SME_TOT9.2, COTTAGE_EXEMPT → nothing to file), output VAT bucketed per VAT rate from issued Mushak 6.3 invoices, input VAT credited from your ledger.

Two things about that return are yours, not Bangladesh's, and they are the only two you wire:

createBdTaxModule({
  connection,
  permissions,
  resolveSeller,                       // issuing party (BIN + legal identity)
  bridges: {
    sales:    orderToTaxableSale,      // order id → neutral taxable sale
    regime:   (ctx) => branchRegimeOf(ctx.organizationId),   // → 9.1 / 9.2 / none
    inputVat: (period, ctx) => ledgerInputVat(period, ctx),  // credit + VDS
  },
});

Both bridges are optional and default safely: no regime bridge means STANDARD_VAT (file a 9.1 — filing an unneeded return is recoverable, silently not filing one is not), and no inputVat bridge means zero credit, so the return still builds before you have a ledger.

Filing periods are resolved in Asia/Dhaka, not UTC (override with filingTimeZone). This matters: Bangladesh is UTC+6, so a UTC month disagrees with the filing month at both ends by six hours, reporting the start of one month as the end of the previous one. Nothing throws when this is wrong.

musokOmitBuiltinReads: true exists for a host that genuinely needs a different statutory shape. Do not reach for it merely to enrich the return — that creates a second implementation of an NBR rule, and the copies drift silently. Wire the bridges instead, or extend the module.

Cloning this for another country

@classytic/ca-tax and @classytic/us-tax already implement the same @classytic/tax-core TaxResolver contract that @classytic/bd-tax does, so a sibling spine module is a composition job, not a rewrite. What you keep and what you replace:

concern reuse as-is why
period boundaries, serial year, YYYY-MM parsing @spinekit/kit/period zone is a parameter; already country-neutral
fiscal position bridges.fiscalPosition port typed as tax-core's signature, so your pack's resolver drops straight in
per-line tax computation your pack (calculateInvoiceTax equivalent), or tax-core's resolver.computeLine the only genuinely country-specific math
ports (SellerResolver, sales/regime/input-tax bridges) shared/ports.ts shapes nothing in them is BD
serial allocation allocateMushakSerial shape atomic per-(scope, year) counter; rename, don't re-derive
idempotent issuance issueMushak shape unique index + duplicate-key read-back; see the CHANGELOG for why the naive version double-issues
per-rate month aggregate MushakRepository.aggregateMonthlyVat shape groups on lines.vatRate; no BD assumption
the MODEL, the FORMS, regime→form dispatch rewrite Mushak 6.3/9.1/9.2 are Bangladesh; GST/HST returns are not

src/pack.ts is the complete list of jurisdiction literals — country code, filing timezone, currency. Nothing else in the module hardcodes any of them, so that file plus the model and forms is your diff. Amounts are minor units and the module only ever sums them, so a currency with a different exponent needs no code change.

License

SEE LICENSE IN LICENSE — Classytic Source-Available License (Community & Commercial). Evaluation and development use are free; production use requires a commercial license. Versions before 1.0.0 remain MIT.

Keywords