kempo-tax 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/PLAN.md +20 -3
- package/README.md +234 -85
- package/admin/components/TaxRates.js +337 -0
- package/admin/components/TaxRegistrations.js +14 -4
- package/admin/components/TaxReports.js +223 -0
- package/admin/components/TaxSettings.js +55 -20
- package/admin/index.page.html +19 -3
- package/install.js +36 -12
- package/kempo-config.json +80 -16
- package/package.json +9 -6
- package/public/api/ledger/GET.js +18 -0
- package/public/api/ledger-export/GET.js +24 -0
- package/public/api/rate/[id]/DELETE.js +17 -0
- package/public/api/rate/[id]/PUT.js +18 -0
- package/public/api/rate-export/GET.js +23 -0
- package/public/api/rate-import/POST.js +25 -0
- package/public/api/rate-quickstart/POST.js +40 -0
- package/public/api/rates/GET.js +16 -0
- package/public/api/rates/POST.js +17 -0
- package/public/api/report-export/GET.js +36 -0
- package/public/api/reports/GET.js +26 -0
- package/public/api/settings/PUT.js +31 -8
- package/public/sdk.js +39 -0
- package/sdk.js +18 -4
- package/server/db/schema.js +137 -0
- package/server/providers/index.js +10 -5
- package/server/providers/rates.js +114 -0
- package/server/utils/config/settings.js +46 -17
- package/server/utils/ledger/csv.js +74 -0
- package/server/utils/ledger/entries.js +129 -0
- package/server/utils/ledger/exports.js +56 -0
- package/server/utils/ledger/money.js +21 -0
- package/server/utils/ledger/period.js +36 -0
- package/server/utils/ledger/quotes.js +69 -0
- package/server/utils/ledger/report.js +155 -0
- package/server/utils/ledger/store.js +150 -0
- package/server/utils/rates/csv.js +53 -0
- package/server/utils/rates/quickstart.js +89 -0
- package/server/utils/rates/store.js +128 -0
- package/server/utils/rates/validate.js +110 -0
- package/server/utils/tax/calculate.js +25 -8
- package/server/utils/tax/percent.js +98 -0
- package/server/utils/tax/rates.js +292 -0
- package/server/utils/tax/shape.js +73 -26
- package/server/utils/tax/transactions.js +161 -20
- package/uninstall.js +33 -0
package/PLAN.md
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
# kempo-tax — PLAN: rate table provider, ledger and reports
|
|
2
2
|
|
|
3
|
-
> Status: **
|
|
4
|
-
>
|
|
5
|
-
>
|
|
3
|
+
> Status: **built (this plan is implemented).** The rate table provider, the ledger, reports, the
|
|
4
|
+
> home-state quick start, rate CSV import/export, the home-sale warning and the uninstall warning
|
|
5
|
+
> are all in. Two items remain and are listed under "Outstanding" at the end of this file. The text
|
|
6
|
+
> below is kept as the design record — read it before changing the behaviour it describes.
|
|
6
7
|
|
|
7
8
|
> **Upstream changes are allowed.** Every dependency with `kempo` in its name (kempo, kempo-ui,
|
|
8
9
|
> kempo-css, kempo-server, kempo-payments, kempo-media, and the rest) is owned by the same person, and
|
|
@@ -156,3 +157,19 @@ Follow `~/dev/kempo/AGENTS.md` and `~/dev/kempo-payments`. Specifically:
|
|
|
156
157
|
- Uninstall warns and export works.
|
|
157
158
|
- Home wizard and rate editor loaded and checked in a browser.
|
|
158
159
|
- README and CHANGELOG updated. Published as a **minor** version.
|
|
160
|
+
|
|
161
|
+
## Outstanding
|
|
162
|
+
|
|
163
|
+
Not done, and not possible from a development checkout:
|
|
164
|
+
|
|
165
|
+
- **Run the Stripe provider against a live test-mode account.** It has only been written against
|
|
166
|
+
the SDK's types and tested for request/response shaping. The manual procedure is in the README.
|
|
167
|
+
- **Verify every `txcd_` id behind the class names** (especially `saas` and `saas-business`)
|
|
168
|
+
against Stripe's own list at `/tax/api/tax-codes`. They were written from memory.
|
|
169
|
+
|
|
170
|
+
Deliberately left for later:
|
|
171
|
+
|
|
172
|
+
- Exemption certificates and resale handling.
|
|
173
|
+
- A third provider (TaxJar, Avalara).
|
|
174
|
+
- An origin-sourcing rule richer than "the seller's own state, from the seller's own address".
|
|
175
|
+
- Fiscal-year periods as a first-class choice (a custom range does the job today).
|
package/README.md
CHANGED
|
@@ -1,27 +1,39 @@
|
|
|
1
1
|
# kempo-tax
|
|
2
2
|
|
|
3
3
|
Sales tax, VAT and GST for the [kempo](https://github.com/dustinpoissant/kempo) CMS. Hand it an
|
|
4
|
-
address and some items, get back what tax to charge —
|
|
4
|
+
address and some items, get back what tax to charge — and keep a record of what you collected, in
|
|
5
|
+
the shape a return asks for.
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
Two ways to get the rates, behind one interface:
|
|
8
|
+
|
|
9
|
+
- **Rate table** — rows you maintain: a state rate, a local option, which things are exempt. No
|
|
10
|
+
external service, no per-transaction fee, works with any payment processor. The right choice for
|
|
11
|
+
a seller in one place, or a few.
|
|
12
|
+
- **Stripe Tax** — [Stripe](https://stripe.com/tax) works out rates and rules for you, which is the
|
|
13
|
+
right choice once you sell into enough places that keeping a table is the problem.
|
|
14
|
+
|
|
15
|
+
Switching between them is a setting. Callers do not change.
|
|
9
16
|
|
|
10
17
|
This kempo (CMS) extension depends on no other kempo extension. It has no catalog, no cart and no
|
|
11
|
-
idea what a "product" is — the caller supplies the classification of each line, which is what lets
|
|
12
|
-
subscription, a donation or an invoice use it exactly as a store checkout does. It sits beside
|
|
18
|
+
idea what a "product" is — the caller supplies the classification of each line, which is what lets
|
|
19
|
+
a subscription, a donation or an invoice use it exactly as a store checkout does. It sits beside
|
|
13
20
|
[kempo-payments](https://github.com/dustinpoissant/kempo-payments), not on top of it: one works out
|
|
14
21
|
what is owed, the other moves the money, and either can be swapped without touching the other.
|
|
15
22
|
|
|
23
|
+
> **This extension works out tax and keeps a record. It does not register you with, or file returns
|
|
24
|
+
> to, any tax authority, and it does not pay anything to anyone.** Whether you owe tax, where, and
|
|
25
|
+
> from when depends on where you are based, where your customers are, how much you sell and what you
|
|
26
|
+
> sell. Talk to an accountant, or to your tax authority.
|
|
27
|
+
|
|
16
28
|
## What it is for
|
|
17
29
|
|
|
18
30
|
Other kempo (CMS) extensions.
|
|
19
31
|
|
|
20
32
|
```js
|
|
21
|
-
import { calculateTax, recordTransaction } from 'kempo-tax/sdk';
|
|
33
|
+
import { calculateTax, recordTransaction, reverseTransaction } from 'kempo-tax/sdk';
|
|
22
34
|
|
|
23
35
|
const [error, quote] = await calculateTax({
|
|
24
|
-
address: { country: 'US', state: '
|
|
36
|
+
address: { country: 'US', state: 'VT', postalCode: '05401', city: 'Burlington' },
|
|
25
37
|
lines: [
|
|
26
38
|
{ amount: 4999, reference: 'sku-1', taxClass: 'physical' },
|
|
27
39
|
{ amount: 1500, reference: 'sku-2', quantity: 3, taxClass: 'digital' },
|
|
@@ -31,12 +43,15 @@ const [error, quote] = await calculateTax({
|
|
|
31
43
|
|
|
32
44
|
// quote.tax what is owed
|
|
33
45
|
// quote.total what to charge — hand this to createPayment
|
|
34
|
-
// quote.collecting true when this site
|
|
46
|
+
// quote.collecting true when this site collects tax where the customer is
|
|
35
47
|
// quote.lines[] the tax on each line, matched back by your reference
|
|
36
|
-
// quote.breakdown[] per jurisdiction: name, rate, taxable amount, amount
|
|
48
|
+
// quote.breakdown[] per jurisdiction: name, level, rate, taxable amount, amount
|
|
37
49
|
|
|
38
50
|
// …and once the payment has *succeeded*, not before:
|
|
39
|
-
await recordTransaction({ calculationId: quote.calculationId, reference: order.id });
|
|
51
|
+
const [, sale] = await recordTransaction({ calculationId: quote.calculationId, reference: order.id });
|
|
52
|
+
|
|
53
|
+
// on a refund, whole or part:
|
|
54
|
+
await reverseTransaction({ transactionId: sale.transactionId, reference: `${order.id}-refund-1`, amount: 1000 });
|
|
40
55
|
```
|
|
41
56
|
|
|
42
57
|
All amounts are integers in the currency's smallest unit — `4999` is $49.99 — the same as
|
|
@@ -48,39 +63,102 @@ kempo-payments, so `quote.total` goes straight into `createPayment`.
|
|
|
48
63
|
npm install kempo-tax
|
|
49
64
|
```
|
|
50
65
|
|
|
51
|
-
Install it from **Admin > Extensions**, then open **Tax** in the admin menu
|
|
66
|
+
Install it from **Admin > Extensions**, then open **Tax** in the admin menu.
|
|
52
67
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
still missing).
|
|
57
|
-
3. Register in the places you are required to collect — also in the Stripe dashboard.
|
|
68
|
+
**For the rate table**, open the **Rates** tab and use the quick start: your country and state, the
|
|
69
|
+
rate, which classes are taxable and which exempt, whether shipping is taxed, and any local option
|
|
70
|
+
taxes. It writes ordinary rows you can then edit.
|
|
58
71
|
|
|
59
|
-
|
|
60
|
-
|
|
72
|
+
**For Stripe Tax**, save a Stripe secret key on the **Settings** tab (or install kempo-payments and
|
|
73
|
+
connect Stripe there, and this extension borrows its key), switch on Stripe Tax in the Stripe
|
|
74
|
+
dashboard, and register where you are required to collect. `SETTINGS_ENCRYPTION_KEY` must be set on
|
|
75
|
+
the server for a key to be stored.
|
|
61
76
|
|
|
62
77
|
## Connected is not the same as collecting
|
|
63
78
|
|
|
64
|
-
|
|
79
|
+
Both providers answer "what would the tax be" and "are we collecting it here" separately.
|
|
80
|
+
|
|
81
|
+
- **Rate table:** there is a rate row for that place and class, or there is not.
|
|
82
|
+
- **Stripe Tax:** Stripe calculates a rate for any address and only *collects* where your account
|
|
83
|
+
is registered. Everywhere else the tax is `0` with the reason `not_collecting`.
|
|
84
|
+
|
|
85
|
+
So `quote.collecting` distinguishes "this is a 0% rate" from "this site does not collect here". That
|
|
86
|
+
is not a fault: a small seller under every threshold owes nothing in most places. It matters because
|
|
87
|
+
a sale made where you *are* based, with no rate, is the one that makes a return short without
|
|
88
|
+
anything looking wrong — see [Sales at home](#sales-at-home-with-no-tax-collected).
|
|
89
|
+
|
|
90
|
+
## The rate table
|
|
91
|
+
|
|
92
|
+
### Rows are layers
|
|
93
|
+
|
|
94
|
+
A place is taxed in layers that stack — a country, a state, a county, a city, a district. Vermont's
|
|
95
|
+
6% state rate plus a 1% local option in some towns is **two rows**, and a customer in such a town
|
|
96
|
+
pays both.
|
|
97
|
+
|
|
98
|
+
For one line, the rows that apply are chosen like this:
|
|
99
|
+
|
|
100
|
+
1. Rows are filtered to those matching the destination (country, then state, town, postal pattern —
|
|
101
|
+
each blank meaning "any"), the line's class (or `*`), and the date.
|
|
102
|
+
2. Within one **level**, the most specific row wins: a postal-code row beats a town row beats a
|
|
103
|
+
state-wide row. At equal specificity a row for a named class beats a `*` row, then higher
|
|
104
|
+
`priority`, then the later `effectiveFrom`, then the newer row.
|
|
105
|
+
3. The winners, one per level, are the layers that apply, and their rates add.
|
|
106
|
+
|
|
107
|
+
A line with **no matching row at any level** is `not_collecting`. An **exempt** row (kind `exempt`)
|
|
108
|
+
is different: it says "sold here, no tax", and it is reported on a return as an exempt sale.
|
|
109
|
+
|
|
110
|
+
### A row
|
|
111
|
+
|
|
112
|
+
| Field | |
|
|
113
|
+
|---|---|
|
|
114
|
+
| `country`, `state`, `locality`, `postalPattern` | Where. Blank means any. State uses the same code your checkout sends (`VT`). A postal pattern is a comma list of exact codes, prefixes (`054*`) and ranges (`05400-05499`). |
|
|
115
|
+
| `taxClass` | `physical`, `digital`, `saas`, `saas-business`, `nontaxable`, `shipping`, a `txcd_…` code, or `*` for every class |
|
|
116
|
+
| `kind` | `taxable` or `exempt` |
|
|
117
|
+
| `rate` | A percentage with up to four decimals: `6`, `7.25`, `6.625` |
|
|
118
|
+
| `level` | `country`, `state`, `county`, `city`, `district` or `special` — worked out from the place if left blank |
|
|
119
|
+
| `appliesToShipping` | Whether this row taxes shipping charges. Shipping is only taxed by rows that say so |
|
|
120
|
+
| `effectiveFrom` / `effectiveTo` | `YYYY-MM-DD`. `effectiveTo` is exclusive |
|
|
121
|
+
| `jurisdiction`, `label` | The name a report uses, and the name a receipt may show |
|
|
122
|
+
| `priority`, `enabled`, `note` | |
|
|
123
|
+
|
|
124
|
+
### A rate is ended, not edited
|
|
125
|
+
|
|
126
|
+
When a rate changes, **end** the old row and add a new one. A sale quoted last month must still be
|
|
127
|
+
taxed at last month's rate, and a report has to be reproducible. Edit a row only to fix a mistake.
|
|
128
|
+
|
|
129
|
+
### Rates are not floats
|
|
130
|
+
|
|
131
|
+
A rate is held as an integer count of millionths of a percent (6.625% is `66250`), and tax is
|
|
132
|
+
computed in `BigInt` and rounded half-up. A tax return that does not reconcile by a cent is one
|
|
133
|
+
somebody has to explain.
|
|
134
|
+
|
|
135
|
+
### Rounding
|
|
136
|
+
|
|
137
|
+
Per line by default: each line's tax is rounded, then they are added. `rounding: invoice` rounds
|
|
138
|
+
once per tax layer for the whole sale and spreads the result back over the lines (largest
|
|
139
|
+
remainder), so every line still shows its own share and they sum to the total. Tax-inclusive lines
|
|
140
|
+
are always rounded by themselves. Use whichever your tax authority's instructions describe.
|
|
141
|
+
|
|
142
|
+
### Sourcing
|
|
65
143
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
144
|
+
Destination sourcing — tax by where the goods go — is the default. For a state that taxes an
|
|
145
|
+
in-state sale by where the *seller* is, list it under `origin_sourcing_states` and set your home
|
|
146
|
+
city and postal code: a sale to your own state is then taxed by your address, and everything else
|
|
147
|
+
stays destination-based.
|
|
69
148
|
|
|
70
|
-
|
|
71
|
-
that shows a tax line should show it when `collecting` is true and `tax` is non-zero, and it is why
|
|
72
|
-
the **Where tax is collected** tab leads with a plain-words banner stating which situation the site is
|
|
73
|
-
in rather than an empty table.
|
|
149
|
+
### Import and export
|
|
74
150
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
151
|
+
Export the table as CSV, edit it in a spreadsheet, import it back. An import **only adds** rows,
|
|
152
|
+
and if any row is wrong nothing is added and the error names the row. A misspelt column is an error,
|
|
153
|
+
not silently dropped.
|
|
154
|
+
|
|
155
|
+
> Vermont's local option taxes and what it exempts are not shipped in this package. Rates change,
|
|
156
|
+
> and stale ones are worse than none. Take them from the Vermont Department of Taxes' own published
|
|
157
|
+
> list and enter or import them.
|
|
79
158
|
|
|
80
159
|
## Tax classes
|
|
81
160
|
|
|
82
|
-
Say what *kind* of thing a line is
|
|
83
|
-
place.
|
|
161
|
+
Say what *kind* of thing a line is.
|
|
84
162
|
|
|
85
163
|
| Class | Meaning |
|
|
86
164
|
|---|---|
|
|
@@ -91,106 +169,177 @@ place.
|
|
|
91
169
|
| `nontaxable` | Never taxed |
|
|
92
170
|
| `shipping` | Shipping and delivery charges |
|
|
93
171
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
172
|
+
A rate-table site matches rows by class name. Stripe uses the category behind the name. A line that
|
|
173
|
+
names no class uses the **default tax class** setting — a class name or a Stripe category id — and
|
|
174
|
+
shipping always uses `shipping`.
|
|
97
175
|
|
|
98
176
|
## Prices that already include tax
|
|
99
177
|
|
|
100
|
-
Off by default, which is US-style pricing: tax is added on top. Turn **Prices already include tax**
|
|
101
|
-
for VAT-style pricing, where the sticker price is what the customer pays
|
|
102
|
-
|
|
178
|
+
Off by default, which is US-style pricing: tax is added on top. Turn **Prices already include tax**
|
|
179
|
+
on for VAT-style pricing, where the sticker price is what the customer pays. A single line can
|
|
180
|
+
override with `taxBehavior: 'inclusive' | 'exclusive'`.
|
|
103
181
|
|
|
104
|
-
In the result, `taxAmountExclusive` is tax to *add* and `taxAmountInclusive` is tax already
|
|
105
|
-
the amounts. `tax` is their sum and `total` is what is actually charged.
|
|
182
|
+
In the result, `taxAmountExclusive` is tax to *add* and `taxAmountInclusive` is tax already
|
|
183
|
+
*inside* the amounts. `tax` is their sum and `total` is what is actually charged.
|
|
106
184
|
|
|
107
|
-
##
|
|
185
|
+
## Recording, reversing and the ledger
|
|
108
186
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
if you need it verified, do that before asking.
|
|
187
|
+
A calculation is a quote. It is safe to ask as often as the page changes, and nothing is reported
|
|
188
|
+
anywhere. It only *counts* once you record it.
|
|
112
189
|
|
|
113
|
-
|
|
190
|
+
Recording writes a row to this site's **ledger** — for every provider, so a return can be produced
|
|
191
|
+
without depending on any third party, and so history survives a change of provider. With Stripe it
|
|
192
|
+
also creates the Stripe transaction that appears in their reports.
|
|
114
193
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
194
|
+
- **The figures come from the stored quote, never from the caller.** You pass a `calculationId` and
|
|
195
|
+
your own `reference`; you cannot pass an amount.
|
|
196
|
+
- **Idempotent on `reference`.** Recording the same order twice — a webhook delivered twice, a retry
|
|
197
|
+
after a timeout — returns the first entry. This is also enforced by a unique index.
|
|
198
|
+
- **One calculation records one sale.** The same `calculationId` under a second reference is refused.
|
|
199
|
+
- **Reversals** take the sale's `transactionId`, a `reference` of their own, and optionally an
|
|
200
|
+
`amount` (of the tax-inclusive total). Omit it to reverse whatever is left. They are stored as
|
|
201
|
+
negative entries, always satisfy `sales + tax = −amount` exactly, and cannot exceed what is left.
|
|
202
|
+
- **Entries are immutable.** A mistake is corrected by a reversal and a new sale.
|
|
118
203
|
|
|
119
|
-
|
|
120
|
-
// when the payment has succeeded:
|
|
121
|
-
const [, sale] = await recordTransaction({ calculationId: quote.calculationId, reference: order.id });
|
|
204
|
+
A quote is kept for 90 days, then pruned — it holds a customer's address.
|
|
122
205
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
206
|
+
### Hooks
|
|
207
|
+
|
|
208
|
+
Fired in notification mode through kempo's `triggerHook`. Register handlers in your own
|
|
209
|
+
`kempo-config.json`.
|
|
210
|
+
|
|
211
|
+
| Hook | Data |
|
|
212
|
+
|---|---|
|
|
213
|
+
| `tax:recorded` | `{ entry }` — a sale was recorded (not on a repeat) |
|
|
214
|
+
| `tax:reversed` | `{ entry, original }` — a reversal was recorded |
|
|
215
|
+
| `tax:uncollected-home-sale` | `{ entry }` — see below |
|
|
216
|
+
|
|
217
|
+
## Reports
|
|
218
|
+
|
|
219
|
+
**Admin > Tax > Reports** builds totals for a quarter or any date range, by currency and by
|
|
220
|
+
jurisdiction, and exports them as CSV. The screen and the CSV are built from one report object, so
|
|
221
|
+
they cannot disagree.
|
|
127
222
|
|
|
128
|
-
|
|
129
|
-
|
|
223
|
+
| Figure | Meaning |
|
|
224
|
+
|---|---|
|
|
225
|
+
| Sales | Amounts, without tax |
|
|
226
|
+
| Taxable | Sales that tax was charged on |
|
|
227
|
+
| Exempt | Sold where you collect, carrying no tax — **still reported** |
|
|
228
|
+
| Not collected | Sold where you have no rate — appears on **no** return |
|
|
229
|
+
| Tax collected / reversed / net | Sales and refunds, net |
|
|
230
|
+
|
|
231
|
+
- **A refund counts in the period it happened in**, not the period of the original sale.
|
|
232
|
+
- **Sales in different currencies are never added together.**
|
|
233
|
+
- **It is a record, not a filing.** The screen says so.
|
|
234
|
+
- Quarters are calendar quarters in UTC. For a fiscal calendar use a custom range (`to` is
|
|
235
|
+
exclusive).
|
|
236
|
+
|
|
237
|
+
### Sales at home with no tax collected
|
|
238
|
+
|
|
239
|
+
If you set the country and state you are based in, then a sale **to there** that is recorded with no
|
|
240
|
+
tax collected fires `tax:uncollected-home-sale` and logs a warning. That is the case that makes a
|
|
241
|
+
return short silently: the rate was never entered, or the registration was never added. It cannot
|
|
242
|
+
catch a sale that was never recorded.
|
|
243
|
+
|
|
244
|
+
## Uninstalling
|
|
245
|
+
|
|
246
|
+
**The ledger is a legal record.** kempo drops this extension's tables on uninstall, and reinstalling
|
|
247
|
+
does not bring the rows back. Uninstall prints how many entries and rates are about to be lost and
|
|
248
|
+
points at the export. Export the ledger first: **Admin > Tax > Reports > Export the whole ledger**
|
|
249
|
+
(or `GET /tax/api/ledger-export`).
|
|
130
250
|
|
|
131
251
|
## SDK
|
|
132
252
|
|
|
133
253
|
| Function | Does |
|
|
134
254
|
|---|---|
|
|
135
255
|
| `calculateTax(request)` | A quote for an address and lines |
|
|
136
|
-
| `recordTransaction({ calculationId, reference })` |
|
|
256
|
+
| `recordTransaction({ calculationId, reference })` | Record a finished sale in the ledger |
|
|
137
257
|
| `reverseTransaction({ transactionId, reference, amount? })` | Reverse some or all of a recorded sale |
|
|
138
|
-
| `
|
|
258
|
+
| `listEntries`, `entriesInPeriod`, `buildReport`, `quarterRange` | Read the ledger and build reports |
|
|
259
|
+
| `listRates`, `createRate`, `updateRate`, `deleteRate`, `cleanRate` | Manage the rate table from code |
|
|
260
|
+
| `TAX_CLASSES`, `resolveTaxClass`, `parseAddress`, `parsePercent` | Validation, for callers that want to check first |
|
|
139
261
|
|
|
140
|
-
Everything returns `[error, data]` with `error` as `{ code, msg }`, like the rest of kempo. These
|
|
141
|
-
the data operations with no permission checks of their own — the routes enforce who may do what.
|
|
262
|
+
Everything returns `[error, data]` with `error` as `{ code, msg }`, like the rest of kempo. These
|
|
263
|
+
are the data operations with no permission checks of their own — the routes enforce who may do what.
|
|
142
264
|
|
|
143
265
|
### Request
|
|
144
266
|
|
|
145
267
|
| Field | Notes |
|
|
146
268
|
|---|---|
|
|
147
269
|
| `address` | `{ country (required, 2 letters), state, postalCode, city, line1 }` |
|
|
148
|
-
| `addressSource` | `'shipping'` (default) or `'billing'`
|
|
270
|
+
| `addressSource` | `'shipping'` (default) or `'billing'` |
|
|
149
271
|
| `lines[]` | `{ amount, reference?, quantity?, taxClass?, taxBehavior? }` — `amount` is the line's **total** |
|
|
150
272
|
| `shipping` | `{ amount, taxClass?, taxBehavior? }` |
|
|
151
273
|
| `currency` | Defaults to the setting |
|
|
152
|
-
| `taxIds` | `[{ type, value }]` |
|
|
153
|
-
| `taxDate` | Unix seconds — use rules in force on that day
|
|
274
|
+
| `taxIds` | `[{ type, value }]` — Stripe only; may make a sale B2B / reverse-charge |
|
|
275
|
+
| `taxDate` | Unix seconds — use the rules in force on that day |
|
|
154
276
|
|
|
155
|
-
A line with an `amount` of `0` is kept in the answer, at zero tax, and is never sent. A cart of
|
|
156
|
-
but free items is answered without calling
|
|
277
|
+
A line with an `amount` of `0` is kept in the answer, at zero tax, and is never sent. A cart of
|
|
278
|
+
nothing but free items is answered without calling anything, and has no `calculationId` — there is
|
|
279
|
+
nothing to record.
|
|
280
|
+
|
|
281
|
+
### Result
|
|
282
|
+
|
|
283
|
+
`calculationId` (record from this), `tax`, `total`, `collecting`, `lines[]` (each with `tax`,
|
|
284
|
+
`netAmount`, `status`: `taxed` / `exempt` / `not_collecting` / `zero`), `shipping`, `breakdown[]`,
|
|
285
|
+
`currency`, `provider`, `expiresAt`.
|
|
157
286
|
|
|
158
287
|
## HTTP API
|
|
159
288
|
|
|
160
|
-
Served under `/tax/api/`, and **every route needs a signed-in user holding the permission**. There
|
|
161
|
-
deliberately no anonymous endpoint: each calculation is
|
|
162
|
-
anyone spend them
|
|
289
|
+
Served under `/tax/api/`, and **every route needs a signed-in user holding the permission**. There
|
|
290
|
+
is deliberately no anonymous endpoint: with Stripe each calculation is an API call and an open one
|
|
291
|
+
would let anyone spend them, and with either provider it would let anyone fill the quote table. A
|
|
292
|
+
storefront shows its customers a tax figure by asking from its own server through the SDK.
|
|
163
293
|
|
|
164
294
|
| Route | Permission |
|
|
165
295
|
|---|---|
|
|
166
296
|
| `POST /calculate` | `tax:calculate` |
|
|
167
|
-
| `POST /transactions/record` | `tax:record` |
|
|
168
|
-
| `
|
|
169
|
-
| `
|
|
297
|
+
| `POST /transactions/record`, `POST /transactions/reverse` | `tax:record` |
|
|
298
|
+
| `GET /rates`, `GET /rate-export`, `GET /status`, `GET /registrations`, `GET /tax-codes` | `tax:view` |
|
|
299
|
+
| `POST /rates`, `PUT/DELETE /rate/:id`, `POST /rate-import`, `POST /rate-quickstart` | `tax:rates` |
|
|
300
|
+
| `GET /ledger`, `GET /ledger-export`, `GET /reports`, `GET /report-export` | `tax:reports` |
|
|
170
301
|
| `GET /settings`, `PUT /settings` | `tax:settings` |
|
|
171
302
|
|
|
172
|
-
|
|
303
|
+
Groups: `kempo-tax:viewer`, `kempo-tax:bookkeeper` (what an order system or accountant acts as) and
|
|
304
|
+
`kempo-tax:administrator`. Browser client: `import { calculateTax } from '/tax/sdk.js'`.
|
|
173
305
|
|
|
174
306
|
## Failing loudly
|
|
175
307
|
|
|
176
308
|
Not connected, or Stripe unreachable, is an **error**, never a quiet zero. A checkout that silently
|
|
177
|
-
stops charging tax for a week because a key expired is a liability that grows with every order; a
|
|
178
|
-
failure is fixed the same afternoon. Decide in the caller what to do with one — most checkouts
|
|
179
|
-
refuse to complete.
|
|
309
|
+
stops charging tax for a week because a key expired is a liability that grows with every order; a
|
|
310
|
+
loud failure is fixed the same afternoon. Decide in the caller what to do with one — most checkouts
|
|
311
|
+
should refuse to complete.
|
|
180
312
|
|
|
181
313
|
## Providers
|
|
182
314
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
one jurisdiction are the intended extensibility targets — each a new file there, not a change to
|
|
315
|
+
`rates` and `stripe`, behind a small interface (`server/providers/index.js` documents it). TaxJar
|
|
316
|
+
and Avalara are the intended extensibility targets — each a new file there, not a change to
|
|
186
317
|
anything that ever called `calculateTax`.
|
|
187
318
|
|
|
319
|
+
### Verifying the Stripe provider
|
|
320
|
+
|
|
321
|
+
The Stripe module is written against the installed SDK's own type definitions and its request and
|
|
322
|
+
response shaping is covered by tests, but **the default test run never calls Stripe**, and it has
|
|
323
|
+
not been exercised against a live account in this repository. Before relying on it:
|
|
324
|
+
|
|
325
|
+
1. Use a **test-mode** key from an account with Stripe Tax switched on, and a head office address.
|
|
326
|
+
2. On **Settings**, save the key, set provider to Stripe, and open the **Where tax is collected**
|
|
327
|
+
tab — it should report the account as active.
|
|
328
|
+
3. Add a test registration for a place in the dashboard, then **Try a calculation** for an address
|
|
329
|
+
there. Expect `collecting: true` and a rate. For an unregistered place expect `collecting: false`.
|
|
330
|
+
4. Record the sale through the API and confirm the transaction appears under *Tax > Transactions* in
|
|
331
|
+
the Stripe dashboard, then reverse it.
|
|
332
|
+
5. Open **Settings** and compare each class name against the category Stripe lists
|
|
333
|
+
(`GET /tax/api/tax-codes`). The ids behind `saas` and `saas-business` in particular should be
|
|
334
|
+
checked before selling under them.
|
|
335
|
+
|
|
188
336
|
## Not here
|
|
189
337
|
|
|
190
|
-
- **Filing returns,
|
|
338
|
+
- **Filing returns, registering, and paying.**
|
|
191
339
|
- **An address book.** The caller owns addresses; this takes a plain object.
|
|
192
|
-
- **Shipping rates.**
|
|
193
|
-
- **
|
|
340
|
+
- **Shipping rates.** This works out tax *on* a shipping charge, not what to charge.
|
|
341
|
+
- **Exemption certificates and resale handling.**
|
|
342
|
+
- **A bundled rate dataset.** Deliberately — see above.
|
|
194
343
|
|
|
195
344
|
## Development
|
|
196
345
|
|
|
@@ -200,6 +349,6 @@ npm run link:local # symlink sibling kempo checkouts; never commit a file: spe
|
|
|
200
349
|
npm test
|
|
201
350
|
```
|
|
202
351
|
|
|
203
|
-
|
|
204
|
-
package's test dependencies; to run them as CI does, install the registry copies instead:
|
|
352
|
+
Tests run against an in-memory Postgres (PGlite) and never call Stripe. A symlinked `kempo` cannot
|
|
353
|
+
see this package's test dependencies; to run them as CI does, install the registry copies instead:
|
|
205
354
|
`npm install --no-save --no-legacy-peer-deps --force kempo@latest kempo-ui@latest kempo-server@latest`.
|