Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ node_modules
.venv

.codex
.claude
AGENTS.md
CLAUDE.md

# test artifacts
data/
Expand Down
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ test: teststatic

teststatic:
node tests/check_static_load.js
node tests/test_tax.js

test-e2e:
"$(abspath ../../../node_modules/.bin/playwright)" test --config "$(CURDIR)/tests/e2e/playwright.config.ts"
Expand Down
6 changes: 5 additions & 1 deletion static/js/tpos.js
Original file line number Diff line number Diff line change
Expand Up @@ -631,7 +631,11 @@ window.app = Vue.createApp({
for (let item of this.cart.values()) {
let tax = item.tax || this.taxDefault
if (tax > 0) {
total += item.price * item.quantity * (tax * 0.01)
const gross = item.price * item.quantity
const taxRate = tax * 0.01
total += this.taxInclusive
? (gross * taxRate) / (1 + taxRate)
: gross * taxRate
}
}
this.cartTax = roundTposCurrencyAmount(total, this.currency)
Expand Down
29 changes: 29 additions & 0 deletions tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,35 @@ async def test_tpos_crud_settings_and_wrapper_token(client: AsyncClient):
assert await get_tpos(tpos["id"]) is None


@pytest.mark.asyncio
async def test_manual_invoice_tax_value_extracts_inclusive_tax(
client: AsyncClient,
):
_user, wallet = await _user_with_tabs("manualtaxtest")
headers = {"X-API-KEY": wallet.adminkey}
create = await client.post(
"/tpos/api/v1/tposs",
json=_tpos_payload(currency="EUR", tax_default=21),
headers=headers,
)
assert create.status_code == 201
tpos = create.json()

response = await client.post(
f"/tpos/api/v1/tposs/{tpos['id']}/invoices",
json={"amount": 350, "exchange_rate": 100},
)
assert response.status_code == 201

payment = await get_standalone_payment(
response.json()["payment_hash"], incoming=True
)
assert payment is not None
details = payment.extra["details"]
assert details["taxIncluded"] is True
assert details["taxValue"] == pytest.approx(3.5 * 0.21 / 1.21)


@pytest.mark.asyncio
async def test_tabs_endpoints_use_real_tabs_api(client: AsyncClient):
_user, wallet = await _user_with_tabs("tabsuser")
Expand Down
100 changes: 100 additions & 0 deletions tests/test_tax.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
const assert = require('node:assert/strict')
const fs = require('node:fs')
const path = require('node:path')
const vm = require('node:vm')

const root = path.resolve(__dirname, '..')
const sandbox = {
Intl,
Math,
Number,
Object,
Array,
Date,
JSON,
Map,
Set,
Promise,
console,
windowMixin: {},
tpos: {},
i18n: {global: {locale: 'en-US'}},
Vue: {
createApp(config) {
return config
}
}
}
sandbox.window = sandbox
sandbox.globalThis = sandbox

const context = vm.createContext(sandbox)
for (const file of ['static/js/tpos-utils.js', 'static/js/tpos.js']) {
vm.runInContext(fs.readFileSync(path.join(root, file), 'utf8'), context, {
filename: file
})
}

const cartTaxTotal = sandbox.app.methods.cartTaxTotal

function taxTotal({items, taxDefault = 0, taxInclusive}) {
const state = {
cart: new Map(items.map((item, index) => [item.id || index, item])),
taxDefault,
taxInclusive,
currency: 'EUR',
cartTax: 0
}
cartTaxTotal.call(state)
return state.cartTax
}

assert.equal(
taxTotal({
items: [{price: 3.5, quantity: 1, tax: 21}],
taxInclusive: true
}),
0.61
)
assert.equal(
taxTotal({
items: [{price: 3.5, quantity: 1, tax: 21}],
taxInclusive: false
}),
0.74
)
assert.equal(
taxTotal({
items: [{price: 3.5, quantity: 2, tax: 21}],
taxInclusive: true
}),
1.21
)
assert.equal(
taxTotal({
items: [
{price: 10, quantity: 1, tax: 10},
{price: 5, quantity: 2, tax: 20}
],
taxInclusive: true
}),
2.58
)
assert.equal(
taxTotal({
items: [{price: 3.5, quantity: 1, tax: 0}],
taxDefault: 0,
taxInclusive: true
}),
0
)
assert.equal(
taxTotal({
items: [{price: 3.5, quantity: 1}],
taxDefault: 21,
taxInclusive: true
}),
0.61
)

console.log('Tax calculation tests passed')
10 changes: 4 additions & 6 deletions views_payments.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,12 +81,10 @@ async def api_tpos_create_invoice(

if not data.details:
tax_value = 0.0
if tpos.tax_default:
tax_value = (
(data.amount / data.exchange_rate) * (tpos.tax_default * 0.01)
if data.exchange_rate
else 0.0
)
if tpos.tax_default and data.exchange_rate:
gross_amount = data.amount / data.exchange_rate
tax_rate = tpos.tax_default * 0.01
tax_value = (gross_amount * tax_rate) / (1 + tax_rate)
data.details = {
"currency": tpos.currency,
"exchangeRate": data.exchange_rate,
Expand Down
Loading