fix(invoices): make the Swish QR encode the amount to pay after ROT/RUT deduction (#1685)
* fix(invoices): make the Swish QR encode the amount to pay after ROT/RUT deduction The Swish payment QR on invoice PDFs encoded the pre-deduction invoice total (getDisplayTotal), while the totals block and the invoice email state "Att betala" as total minus the ROT/RUT deduction (getAmountToPay, fakturamodellen). Since the Swish payload locks the amount (editmask 0), a customer scanning a RUT/ROT invoice was asked to pay the full total with no way to correct it: overpaying by the entire skattereduktion. Swap the QR amount source to getAmountToPay(...).toPay so the QR, the printed "Att betala" and the email always agree. A fully deducted invoice (toPay = 0) now renders no QR via the existing amount > 0 guard. All seven render surfaces (send, preview, pdf, v1 send/pdf, MCP commit, recurring, issue-and-book) go through this one helper. Reported by a user: "QR-koden for swish stammer INTE med beloppet man ska betala. Den tar INTE hansyn till reduktionen." Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(invoices): select the amount-to-pay columns on the v1 pdf and send surfaces Skeptic review of the Swish QR fix found it was a silent no-op on the v1 GET pdf route: its column projection predated ROT/RUT and omitted deduction_total (and ore_rounding), so getAmountToPay saw undefined, treated it as "no deduction", and the route kept emitting a locked full-amount QR while the sent email said the deducted "Att betala". INVOICE_FULL_COLUMNS (v1 send renders from it) likewise omitted ore_rounding, ignoring the per-invoice oresavrundning override there. Move INVOICE_PDF_COLUMNS into lib/api/v1/invoice-columns.ts, add deduction_total, deduction_personnummer_last4 and ore_rounding to it, add ore_rounding to INVOICE_FULL_COLUMNS, and pin the amount-path columns of both projections with a test: a projection gap does not error, it renders the wrong money on one surface only, so it must be caught structurally. Also records the defect and remediation in DECISIONS.md per the compliance-swarm change-risk finding (the repo has no risk_register.csv; the decision log is its equivalent). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(invoices): gate the Swish QR to payable documents and restore delivery_date on the v1 pdf Swedish accounting review round 2: buildSwishQrDataUrl had no non-payable gate, so a kreditfaktura (a refund document) still produced a locked Swish payment QR at helper level; the template happens to hide the payment box for credit notes, but a payment request against a refund must stay impossible rather than merely unrendered. Apply the same document gate buildPaymentLinkQrDataUrl already has (invoice documents without credited_invoice_id only) and pin it with tests replacing the credit-note parity case. Also add delivery_date to INVOICE_PDF_COLUMNS: ML 17 kap 24 p.7 requires leveransdatum on the invoice when it differs from the invoice date, the template renders exactly that, and the v1 pdf projection silently dropped it. Same projection-starvation class as the previous commit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(invoices): name the covered render surfaces and drop the contested lagrum point number CodeRabbit round 3, both documentation-only: the DECISIONS defect record said "all surfaces" while the editor preview is deferred to #1686, so it now lists the covered surfaces explicitly; and the delivery_date comment cited ML 17 kap 24 p.7 where CodeRabbit reads p.8 in SFS 2023:200 while the repo's swedish-invoice-compliance reference table says p.7, so the citation drops the point number and stays at the paragraph, which is correct under either enumeration. No behavior change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1047,8 +1047,12 @@ One line per decision: `[YYYY-MM-DD] <decision>: <why>`. Appended by agents and
|
||||
[2026-08-17] 77xx nedskrivningar split per official BAS kopplingstabell in BOTH k2-mapper and ink2-engine (fältkod 7515: 7700-7739, 7750-7789, 7800-7899; 7516: 774x, 779x): agent feedback 2026-07-07 reported the K2 side; the INK2R side and the swedish-sru-filing reference table had the same whole-77xx-to-7516 error, verified against bas.se INK2_P1_intervall-240118.pdf before overriding the skill reference. NE-bilaga mappings deliberately untouched (NE has no separate omsättningstillgångar line).
|
||||
[2026-08-17] MCP feedback loop = local /loop-feedback-triage appending dev_docs/mcp_feedback_digest.md + small PRs, NOT a GitHub-issue digest or Resend email: closes the loops.md backlog item blocked since 07-09 on a "channel decision". Issues stay founder-authorised; the digest is the read surface. gnubok_feedback reply copy no longer promises weekly aggregation (it was never true); tool advertised in server instructions + agent briefing (feedback_channel), where it was previously discoverable only by scanning tools/list.
|
||||
[2026-08-17] Non-IBAN foreign payment accounts (USD/GBP): added generic bank_code + foreign_account_number to InvoicePaymentAccount (JSONB, no migration) instead of per-country fields (routing_number, sort_code, bsb); rule = IBAN OR (bank_code + foreign_account_number + BIC), only for NON_IBAN_CURRENCIES, label per currency. Chosen over a field per country: the Currency union only carries USD/GBP among non-IBAN systems, and one generic pair keeps the PDF/settings/schema surface small; extend NON_IBAN_CURRENCIES + bankCodeLabelKey when AUD/CAD land. Agent feedback 2026-08-03.
|
||||
|
||||
[2026-08-18] Swish QR fix (RUT/ROT) scoped to buildSwishQrDataUrl only, preview-pdf left as-is: previewInvoice carries no deduction fields at all, so the editor preview is internally consistent (its QR matches its own totals block) but ignores ROT/RUT entirely; that is a separate display gap to file as its own issue rather than widening this money-math diff.
|
||||
[2026-08-18] Shopify webshop_orders port: vat_breakdown is reconstructed from the ORDER-LEVEL taxLines (net = tax / rate, remainder as a 0%-bucket, refuse on missing rates or overshoot) instead of summing line items like the WooCommerce sync: Shopify's discountedTotalSet excludes cart-level discount allocations and lineItems is a paginated connection, so part-summing can silently produce a wrong per-rate net, while tax-per-rate and the charged total are authoritative order-level facts. Refund VAT is always prorated from the parent's mix (Shopify's Refund object exposes no per-rate tax without paging refundLineItems per refund).
|
||||
[2026-08-18] Shopify order feed keeps its paid-only qualification (PAID/PARTIALLY_REFUNDED/REFUNDED) after the webshop_orders port, unlike WooCommerce which also imports unpaid orders for the invoice flow: widening qualification is a product decision, out of scope for the port; unpaid orders re-surface via updatedAt when payment captures. The line-item snapshot is stored only when the parts reconstruct the charged total to the ore (else [] and the invoice conversion falls back to one aggregate line), and the bookkeeping-lock row filter was dropped: an Orders-page row behind the lock is an overview row, not permanent inbox noise, and booking is still blocked by the lock triggers (parity with WooCommerce).
|
||||
[2026-08-18] Skattekontoutdrag sum mismatch (opening + events != closing) demoted from a hard 400 to a preview confirm gate showing ingående/händelser/utgående/differens, mirroring the orgnr-mismatch gate: Sebastian's real export was refused on it (2026-08-18) with no way forward and no figures to diagnose; nothing is booked at import and dedup makes a later complete re-import safe, so refusing the file only blocked the rows that WERE readable. Parser also takes the earliest opening / latest closing across several marker pairs, reads a marker saldo from a trailing running-saldo column, and accepts U+2212 / plus-sign amounts; the route logs the figures (amounts and counts, never row text) so the next report is diagnosable from Vercel logs. Kept the hard reject only for zero readable rows.
|
||||
|
||||
[2026-08-18] Defect record (compliance-swarm PI1.3, no risk_register.csv exists in repo so this log is the equivalent): Swish QR on invoice PDFs encoded the pre-deduction total on ROT/RUT invoices with the amount locked (editmask 0), risking customer overpayment by the full skattereduktion; impact window = since Swish QR + ROT/RUT coexisted; remediation PR #1685 = QR now encodes getAmountToPay().toPay on every send/download surface (dashboard send/preview-of-send/pdf, v1 send/pdf, MCP pending-operations commit, recurring, issue-and-book; the editor preview is tracked separately in #1686), v1 pdf/send projections gained the amount-path columns (deduction_total, ore_rounding), pinned by lib/api/v1/__tests__/invoice-columns.test.ts.
|
||||
[2026-08-18] Generic CSV mapping (#1671): description guess now excludes clock-time columns (Time/Tid/Klockslag by label, HH:MM by values) and knows Lunar's Title/Titel label; Lunar detect() sniffs comma/semicolon/tab and matches header CELLS exactly (date, title|text, amount, balance) instead of substrings, aligned with what parse() resolves on. NOT changed: the 2026-08-13 generic_csv exemption from the parsed-0-rows auto-detect fallback stays; lifting it would route an explicit "Annan CSV" pick into a dedicated parser and remove the manual escape hatch. Not verified against the customer's actual file (Gmail thread not readable in-session): the semicolon/tab widening is the plausible detection miss, a Swedish-localized Lunar header is not confirmed to exist and was not special-cased in the Lunar detector (the generic path now maps it correctly anyway).
|
||||
[2026-08-18] Editor PDF preview (#1686) recomputes ROT/RUT server-side from the posted lines with the same helpers as build-invoice-write.ts (computeDeduction / computeInvoiceDeductionTotal, base inkl. moms at the rendered rate, invoice-doc only) and resolves the masked personnummer the same way (typed value, else an individual customer's kundkort personnummer): the client is not trusted with the deduction math, and the preview must state the same avdrag row, info box and "Att betala" as the invoice that gets created. The editor now also posts deduction_personnummer / deduction_housing_designation to the preview route, only when a line claims a deduction (same privacy rule as buildInvoiceWritePayload). Swish QR amount in the preview follows buildSwishQrDataUrl, fixed separately in #1685.
|
||||
|
||||
@@ -30,14 +30,9 @@ import {
|
||||
hasRequiredInvoicePaymentAccount,
|
||||
invoiceRequiresPaymentAccount,
|
||||
} from '@/lib/invoices/payment-accounts'
|
||||
import { INVOICE_PDF_COLUMNS } from '@/lib/api/v1/invoice-columns'
|
||||
import type { CompanySettings, Customer, Invoice, InvoiceItem } from '@/types'
|
||||
|
||||
const INVOICE_PDF_COLUMNS =
|
||||
'id, invoice_number, customer_id, invoice_date, due_date, status, document_type, ' +
|
||||
'currency, subtotal, vat_amount, total, vat_treatment, vat_rate, moms_ruta, ' +
|
||||
'reverse_charge_text, your_reference, our_reference, notes, credited_invoice_id, ' +
|
||||
'paid_amount, remaining_amount'
|
||||
|
||||
const PDF_FETCH_SELECT = `
|
||||
${INVOICE_PDF_COLUMNS},
|
||||
customer:customers(*),
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Guards the v1 invoice projections against silently starving the render
|
||||
* path. getAmountToPay treats a missing column as "no deduction" / "default
|
||||
* rounding" (`deduction_total ?? 0`, `ore_rounding ?? company ?? true`), so a
|
||||
* projection that drops one of its inputs does not error: it renders a PDF
|
||||
* and a locked Swish QR (editmask 0) with the WRONG amount on that surface
|
||||
* only, while the dashboard PDF and the sent email for the same invoice show
|
||||
* the deducted "Att betala". That is the exact bug shipped on the v1 pdf
|
||||
* route when INVOICE_PDF_COLUMNS predated ROT/RUT and nothing pinned it.
|
||||
*/
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { INVOICE_FULL_COLUMNS, INVOICE_PDF_COLUMNS } from '@/lib/api/v1/invoice-columns'
|
||||
|
||||
/** Columns getAmountToPay + the Swish QR/totals path read off the invoice. */
|
||||
const AMOUNT_TO_PAY_COLUMNS = ['total', 'currency', 'ore_rounding', 'deduction_total', 'credited_invoice_id']
|
||||
|
||||
const parse = (projection: string): string[] => projection.split(',').map((c) => c.trim())
|
||||
|
||||
describe('v1 invoice projections feed the render amount path', () => {
|
||||
it.each(AMOUNT_TO_PAY_COLUMNS)('INVOICE_PDF_COLUMNS contains %s', (column) => {
|
||||
expect(parse(INVOICE_PDF_COLUMNS)).toContain(column)
|
||||
})
|
||||
|
||||
it.each(AMOUNT_TO_PAY_COLUMNS)('INVOICE_FULL_COLUMNS contains %s (v1 send renders from it)', (column) => {
|
||||
expect(parse(INVOICE_FULL_COLUMNS)).toContain(column)
|
||||
})
|
||||
|
||||
it('INVOICE_PDF_COLUMNS carries the display-safe personnummer for the PDF deduction box', () => {
|
||||
expect(parse(INVOICE_PDF_COLUMNS)).toContain('deduction_personnummer_last4')
|
||||
})
|
||||
|
||||
it('INVOICE_PDF_COLUMNS carries delivery_date (ML 17 kap 24 §: rendered when it differs from invoice_date)', () => {
|
||||
expect(parse(INVOICE_PDF_COLUMNS)).toContain('delivery_date')
|
||||
})
|
||||
|
||||
it('never selects the encrypted personnummer blob', () => {
|
||||
expect(INVOICE_FULL_COLUMNS).not.toContain('deduction_personnummer_encrypted')
|
||||
expect(INVOICE_PDF_COLUMNS).not.toContain('deduction_personnummer_encrypted')
|
||||
})
|
||||
})
|
||||
@@ -12,7 +12,27 @@
|
||||
*/
|
||||
|
||||
export const INVOICE_FULL_COLUMNS =
|
||||
'id, invoice_number, customer_id, invoice_date, due_date, delivery_date, status, currency, exchange_rate, exchange_rate_date, subtotal, subtotal_sek, vat_amount, vat_amount_sek, total, total_sek, vat_treatment, vat_rate, moms_ruta, your_reference, our_reference, notes, payment_link_url, stripe_payment_link_id, payment_link_auto, reverse_charge_text, credited_invoice_id, document_type, converted_from_id, paid_at, paid_amount, remaining_amount, default_dimensions, deduction_total, deduction_personnummer_last4, created_at, updated_at'
|
||||
'id, invoice_number, customer_id, invoice_date, due_date, delivery_date, status, currency, exchange_rate, exchange_rate_date, subtotal, subtotal_sek, vat_amount, vat_amount_sek, total, total_sek, ore_rounding, vat_treatment, vat_rate, moms_ruta, your_reference, our_reference, notes, payment_link_url, stripe_payment_link_id, payment_link_auto, reverse_charge_text, credited_invoice_id, document_type, converted_from_id, paid_at, paid_amount, remaining_amount, default_dimensions, deduction_total, deduction_personnummer_last4, created_at, updated_at'
|
||||
|
||||
/**
|
||||
* Projection for the v1 PDF download route. Narrower than INVOICE_FULL_COLUMNS
|
||||
* (no payment-link/dimension internals), but it MUST contain every column the
|
||||
* render path reads to compute the customer-facing amount: getAmountToPay
|
||||
* derives "Att betala" from total, ore_rounding, deduction_total and
|
||||
* credited_invoice_id, and the same figure is locked into the Swish QR
|
||||
* (editmask 0). A column missing here silently zeroes that part of the
|
||||
* calculation for this surface only: the v1 PDF then disagrees with the
|
||||
* dashboard PDF and the sent email for the same invoice, which is exactly
|
||||
* the byte-equivalence this endpoint promises. delivery_date is statutory
|
||||
* content on top of that: ML 17 kap 24 § requires leveransdatum on the
|
||||
* invoice when it differs from the invoice date, and the template renders it
|
||||
* exactly then. Pinned by __tests__/invoice-columns.test.ts.
|
||||
*/
|
||||
export const INVOICE_PDF_COLUMNS =
|
||||
'id, invoice_number, customer_id, invoice_date, due_date, delivery_date, status, document_type, ' +
|
||||
'currency, subtotal, vat_amount, total, ore_rounding, vat_treatment, vat_rate, moms_ruta, ' +
|
||||
'reverse_charge_text, your_reference, our_reference, notes, credited_invoice_id, ' +
|
||||
'paid_amount, remaining_amount, deduction_total, deduction_personnummer_last4'
|
||||
|
||||
export const INVOICE_ITEM_FULL_COLUMNS =
|
||||
'id, sort_order, line_type, description, quantity, unit, unit_price, line_total, vat_rate, vat_amount, article_id, revenue_account, deduction_type, deduction_amount, labor_hours, work_type, housing_designation, apartment_number, brf_org_number, dimensions, created_at'
|
||||
|
||||
@@ -11,11 +11,12 @@
|
||||
* mock `fetch` and exercise the real sharp pipeline.
|
||||
*/
|
||||
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi, type MockInstance } from 'vitest'
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { join } from 'node:path'
|
||||
import sharp from 'sharp'
|
||||
import { prepareInvoicePdfRender, buildPaymentLinkQrDataUrl } from '@/lib/invoices/pdf-render-helpers'
|
||||
import QRCode from 'qrcode'
|
||||
import { prepareInvoicePdfRender, buildPaymentLinkQrDataUrl, buildSwishQrDataUrl } from '@/lib/invoices/pdf-render-helpers'
|
||||
import { makeCompanySettings, makeInvoice } from '@/tests/helpers'
|
||||
|
||||
const fontDownloadMock = vi.hoisted(() => vi.fn())
|
||||
@@ -336,3 +337,54 @@ describe('buildPaymentLinkQrDataUrl', () => {
|
||||
).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('buildSwishQrDataUrl', () => {
|
||||
// Spy without replacing the implementation: the payload string is the unit
|
||||
// under test (the PNG pixels are qrcode's concern), and the passthrough
|
||||
// keeps the returned data URL real. The Swish payload locks the amount
|
||||
// (editmask 0), so an amount above "Att betala" makes the customer overpay
|
||||
// with no way to correct it in the app.
|
||||
let toDataURL: MockInstance
|
||||
|
||||
beforeEach(() => {
|
||||
toDataURL = vi.spyOn(QRCode, 'toDataURL')
|
||||
})
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
const company = () => makeCompanySettings({ swish: '1234567890' })
|
||||
|
||||
it('encodes "Att betala" (total minus ROT/RUT deduction), not the invoice total', async () => {
|
||||
const invoice = makeInvoice({ total: 1250, deduction_total: 625 })
|
||||
const qr = await buildSwishQrDataUrl(company(), invoice)
|
||||
expect(qr).toMatch(new RegExp(`^${PNG_DATA_URL_PREFIX}`))
|
||||
expect(toDataURL).toHaveBeenCalledWith('C1234567890;625.00;F-2024001;0', expect.anything())
|
||||
})
|
||||
|
||||
it('applies öresavrundning before the deduction, same order as the PDF totals block', async () => {
|
||||
const invoice = makeInvoice({ total: 1250.49, ore_rounding: true, deduction_total: 625 })
|
||||
await buildSwishQrDataUrl(company(), invoice)
|
||||
expect(toDataURL).toHaveBeenCalledWith('C1234567890;625.00;F-2024001;0', expect.anything())
|
||||
})
|
||||
|
||||
it('renders no QR when the deduction covers the whole invoice (nothing to pay)', async () => {
|
||||
const invoice = makeInvoice({ total: 1250, deduction_total: 1250 })
|
||||
expect(await buildSwishQrDataUrl(company(), invoice)).toBeNull()
|
||||
expect(toDataURL).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('renders no QR for non-payable documents (credit note, proforma, delivery note)', async () => {
|
||||
const creditNote = makeInvoice({ total: 1250, deduction_total: 625, credited_invoice_id: 'inv-orig' })
|
||||
expect(await buildSwishQrDataUrl(company(), creditNote)).toBeNull()
|
||||
expect(await buildSwishQrDataUrl(company(), makeInvoice({ document_type: 'proforma' }))).toBeNull()
|
||||
expect(await buildSwishQrDataUrl(company(), makeInvoice({ document_type: 'delivery_note' }))).toBeNull()
|
||||
expect(toDataURL).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('keeps the plain total for invoices without a deduction', async () => {
|
||||
const invoice = makeInvoice()
|
||||
await buildSwishQrDataUrl(company(), invoice)
|
||||
expect(toDataURL).toHaveBeenCalledWith('C1234567890;12500.00;F-2024001;0', expect.anything())
|
||||
})
|
||||
})
|
||||
|
||||
@@ -23,7 +23,7 @@ import QRCode from 'qrcode'
|
||||
import type { CompanySettings, Currency, Invoice } from '@/types'
|
||||
import { brandingFromCompanySettings, SHOW_SWISH_ON_INVOICE, type InvoiceBranding } from '@/lib/invoices/pdf-template'
|
||||
import { buildSwishQrPayload } from '@/lib/payments/swish'
|
||||
import { getDisplayTotal } from '@/lib/invoices/rounding'
|
||||
import { getAmountToPay } from '@/lib/invoices/rounding'
|
||||
import { createLogger } from '@/lib/logger'
|
||||
import { LOGO_UPLOAD_MAX_BYTES } from '@/lib/invoices/branding-constants'
|
||||
import { prepareInvoiceFont } from '@/lib/invoices/pdf-fonts'
|
||||
@@ -175,14 +175,6 @@ export async function prepareInvoicePdfRender(
|
||||
return { branding, company: resolved }
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the Swish payment QR for an invoice as a PNG data URL, or null when:
|
||||
* Swish display is off, there's no/invalid Swish number, the invoice isn't in
|
||||
* SEK (Swish is SEK-only), or the amount is not positive. Generated locally with
|
||||
* the `qrcode` lib: no call to any Swish API. Pass the result to InvoicePDF's
|
||||
* `swishQrDataUrl` prop; the template gates rendering on the same payment box
|
||||
* that already shows the Swish number.
|
||||
*/
|
||||
/**
|
||||
* Build the payment-link QR for an invoice as a PNG data URL, or null when the
|
||||
* invoice carries no payment_link_url or it isn't a payable document (credit
|
||||
@@ -206,6 +198,20 @@ export async function buildPaymentLinkQrDataUrl(invoice: Invoice): Promise<strin
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the Swish payment QR for an invoice as a PNG data URL, or null when:
|
||||
* Swish display is off, the document isn't a payable invoice (credit notes,
|
||||
* proformas and delivery notes collect no payment), there's no/invalid Swish
|
||||
* number, the invoice isn't in SEK (Swish is SEK-only), or the amount to pay
|
||||
* is not positive. The encoded amount is the customer-facing "Att betala"
|
||||
* from getAmountToPay: the rounded total minus any ROT/RUT deduction, the
|
||||
* same figure the PDF totals block and the invoice email state. The Swish payload locks the amount (editmask 0),
|
||||
* so encoding anything else makes the customer overpay with no way to correct
|
||||
* it in the app. A fully deducted invoice (toPay = 0) therefore renders no QR.
|
||||
* Generated locally with the `qrcode` lib: no call to any Swish API. Pass the
|
||||
* result to InvoicePDF's `swishQrDataUrl` prop; the template gates rendering
|
||||
* on the same payment box that already shows the Swish number.
|
||||
*/
|
||||
export async function buildSwishQrDataUrl(
|
||||
company: CompanySettings,
|
||||
invoice: Invoice,
|
||||
@@ -216,11 +222,17 @@ export async function buildSwishQrDataUrl(
|
||||
// Swish display off is the normal "no QR" case: stay quiet. Every other
|
||||
// skip is logged so a missing QR is diagnosable instead of silent.
|
||||
if (!(company.invoice_show_swish ?? false)) return null
|
||||
// Non-payable documents: the PDF hides the whole payment box for them, and
|
||||
// a locked payment QR on a kreditfaktura (a refund document, "Er tillgodo")
|
||||
// must stay impossible even if a template regression ever exposed the
|
||||
// corner. Same gate as buildPaymentLinkQrDataUrl; quiet like display-off.
|
||||
const docType = invoice.document_type || 'invoice'
|
||||
if (docType !== 'invoice' || invoice.credited_invoice_id) return null
|
||||
if ((invoice.currency ?? 'SEK') !== 'SEK') {
|
||||
log.info('swish QR skipped: invoice not in SEK', { invoiceId: invoice.id, currency: invoice.currency })
|
||||
return null
|
||||
}
|
||||
const amount = getDisplayTotal(invoice, company).displayed
|
||||
const amount = getAmountToPay(invoice, company).toPay
|
||||
const payload = buildSwishQrPayload(company.swish, amount, invoice.invoice_number ?? '')
|
||||
if (!payload) {
|
||||
log.warn('swish QR skipped: invalid number or non-positive amount', {
|
||||
|
||||
Reference in New Issue
Block a user