Schema, trust model, and install mechanics for country tax packs. How the engine applies a pack is in taxation; why packs are data rather than plugins is in 0004: data-only tax packs. The authoring procedure is in adding a tax pack.
The types below are declared in main/tax-packs/types.ts.
main/tax-packs/generic.json is the reference instance and the only bundled pack.
A pack is a data object conforming to schemaVersion: 1.
| Field | Type | Notes |
|---|---|---|
schemaVersion |
1 |
Only 1 is accepted at activation. |
id |
string |
Catalog ids match ^[a-z0-9][a-z0-9-]*$. |
publisher |
string |
local is reserved; see below. |
sourceType |
'official' | 'community' |
Optional; defaults to official. |
version |
string |
Must match ^\d+\.\d+\.\d+$. |
country |
string |
Two-letter code, or * for a multi-country pack. |
jurisdiction |
string |
Free text, * for country-wide. |
currency |
string |
ISO 4217, or XXX for a placeholder. |
effectiveFrom |
string |
A parseable date. |
effectiveTo |
string |
Optional; must be at or after effectiveFrom. |
publishedAt |
string |
|
minFloVersion |
string |
Minimum FloCafe version that may activate it. |
taxPoint |
'order_created' | 'finalized_at' |
Declared, but nothing reads it. |
inclusivePricingDefault |
boolean |
|
registrationNumberLabel |
string |
Merchant-facing label. |
registrationNumberFormat |
TaxIdFormat |
Optional pattern plus description. |
categories |
TaxCategory[] |
|
defaultCategories |
Record<TaxLineKind, string> |
A category id for every line kind. |
unclassifiedCategoryId |
string |
Must name a category in categories. |
rules |
TaxRule[] |
|
taxRounding |
TaxRounding |
|
payableRounding |
PayableRounding |
TaxCategory is { id, label, ruleIds, defaultBehavior? }, where defaultBehavior is one of
country_default, inclusive, exclusive, or exempt.
TaxLineKind is product, packaging, delivery, service_charge, or addon. Every one of
those needs an entry in defaultCategories.
TaxRounding is { scope, method, decimalPlaces, remainderAllocation }:
scope:unit,line, ordocument.method:half_up,half_even,floor, orceiling.decimalPlaces: an integer from 0 to 6.remainderAllocation:largest_remainder. It is the only accepted value.
PayableRounding is { increment, method }, where increment is a positive decimal string at most
1000.
TaxRule is { id, label, type, categoryIds, rate?, amount?, appliesPer?, baseRuleIds?, conditions? }.
typeispercentorfixed. Apercentrule carriesrate; afixedrule carriesamountandappliesPer.appliesPerisunitorline. Afixedrule must declare it.baseRuleIdsmakes apercentrule compound, applying to the base rules' result instead of the line gross. Afixedrule may not declare it.categoryIdsnames the categories the rule applies to.
TaxRuleConditions is { businessTypes?, customerStateRelation?, customerExempt? }.
customerStateRelation is interstate or intra_or_unspecified.
Amounts are strings, not numbers. A rate or amount that is not finite, or is negative, fails activation.
A catalog artifact may be either a bare CountryPack or a CountryTaxPackPluginArtifact:
interface CountryTaxPackPluginArtifact {
schemaVersion: 1;
artifactType: 'country-tax-pack-plugin';
id: string;
displayName: string;
publisher: string;
version: string;
country: string;
jurisdiction: string;
publishedAt: string;
minFloVersion: string;
taxPack: CountryPack;
printTemplates?: PluginPrintTemplate[];
}Despite the name, the artifact carries no plugin code. printTemplates is a list of declarative
escpos-line-template-v1 templates rendered by FloCafe code; see
print templates compliance. The templates are data, validated on
install by validateTemplateChargeRows() and validateTemplateLabelsMap() in
main/tax-packs/catalog.ts, and persisted to
installed_print_templates keyed by the installed pack version.
The plugin artifact is what is signed and digested, so a pack and its print templates are distributed and verified as one unit.
installCatalogEntry() in main/routes/tax-packs.ts runs
downloadAndVerifyTaxPack() in main/tax-packs/catalog.ts
first. The steps run in this order; each is a hard gate.
- Catalog fetch.
fetchRemoteTaxPackCatalog()pages the GitHub Releases API forFreeOpenSourcePOS/FloCafe-Plugins, 100 per page, at mostMAX_RELEASE_PAGES(10) pages. A request that exceedsMAX_CATALOG_BYTES(1,000,000) is rejected. - Release filter. Draft releases are skipped, and a release is considered only when its tag
matches
^tax-pack-[a-z0-9][a-z0-9-]*-v\d+\.\d+\.\d+$. Acatalog.jsonasset is then located on that release. - Download URL guard.
trustedReleaseDownloadUrl()accepts a URL only when the protocol ishttps, the host is exactlygithub.com, there are no embedded credentials, and the path starts with/FreeOpenSourcePOS/FloCafe-Plugins/releases/download/. - Size caps. The pack artifact is capped at
MAX_PACK_BYTES(2,000,000) and the detached signature atMAX_SIGNATURE_BYTES(4,096). - Digest. The SHA-256 of the pack JSON must equal the catalog entry's
digest. - Signature. The detached signature must verify as Ed25519 against
TRUSTED_TAX_PACK_SIGNING_PUBLIC_KEY. - Identity cross-check. If a pack with that id is already installed, its
publisher,country, andjurisdictionmust match the downloaded artifact, or install fails with HTTP 409. Installing the same id and version twice also fails with HTTP 409. - Local publisher rejection. A downloaded artifact claiming
publisher: 'local'is refused. - Staging. The pack, its rules, and its print templates are written to the version, its rules,
and
installed_print_templatestables, with astatusofinstalledrather thanactive. - Activation. Activation runs the 26 checks in
taxation and then switches the pack's
active_version_id, flipping the version and its template rows toactive.
A catalog request uses a 15 second timeout and honours an abort signal.
publisher: 'local' is reserved for packs the app synthesizes, which today means only manual tax
configuration. A local pack must carry no signature. The catalog path refuses a downloaded artifact
that claims to be local, so local status cannot be used to skip signing.
sourceType: 'community' marks a pack as built from public information rather than an official
source. It requires merchant acknowledgement of a disclaimer, and a local or manual pack may never
declare it.
The Singapore community profile in
main/tax-packs/community-singapore.json applies
9% GST from 2024-01-01 to standard-rated restaurant products, packaging, delivery, service charges,
add-ons, and unclassified lines. Its default pricing behavior is GST-inclusive. Activation does not
verify GST registration with IRAS; only GST-registered businesses charge GST. The registration field
accepts UENs and IRAS-issued GST registration numbers, which may be separate from a UEN. Its format
check validates syntax only, not registration status.
Hotels and food-and-beverage establishments that impose a genuine service charge may display GST-exclusive prices for goods and services subject to that charge, with a prominent notice. The service charge remains part of the amount on which GST is calculated. The community profile does not model the price-display concession, rate-change transition rules, or exempt and zero-rated supplies. It rounds document GST to cents using half-up rounding. See IRAS guidance on the prevailing 9% rate, hotel and F&B treatment, and GST registration numbers on invoices.
A JSON source file in FloCafe does not make a pack bundled or available in the public catalog. The catalog contains only signed releases that have been published in FloCafe-Plugins.
There is one trusted signing public key, in
main/tax-packs/trusted-signing-key.ts.
LEGACY_TRUSTED_PACK_DIGESTS in main/routes/tax-packs.ts pins four SHA-256 digests of
pre-signing artifacts, for official-india, official-thailand, official-in, and
official-th. An unsigned artifact matching one of them is accepted. This is a compatibility
exception pinned by tests/legacy-tax-pack-digest.test.ts, not a way to publish a new unsigned
pack.
buildManualPack() in main/routes/tax-packs.ts turns settings-entered tax configuration into a
CountryPack with publisher: 'local', id manual-<country>, and jurisdiction *. It installs
through the same path and is calculated by the same TaxEngine.calculate(), so there is no second
calculation path. POST /manual-config is owner-only.
npm run test:tax-pack-catalog
npm run test:tax-pack-management
npm run test:manual-tax-config
npm run test:legacy-tax-pack-digest
npm run test:community-tax-packs