FinBaze for Platforms

Guides

A point-of-sale system produces one number set per till per day: the Z-report. FinBaze books it. You push the Z-report as a custom record; a flow template turns its state change into an invoice and a journal entry, with the same rules as the data API. The POS demo contains the code of this page.

1. Define the record type#

mutation {
  definePlatformRecordType(input: {
    key: "z_report"
    name: "Z-report"
    schema: {
      type: "object"
      required: ["till", "closedAt", "sales"]
      properties: {
        till: { type: "string" }
        closedAt: { type: "string" }
        sales: { type: "array" }
        tipsMinor: { type: "integer" }
        cashDifferenceMinor: { type: "integer" }
      }
    }
    states: ["open", "closed"]
    initialState: "open"
    displayTemplate: "Z-report {{data.till}} {{data.closedAt}}"
  }) { version }
}

Changing the schema or states creates a new version; an identical call returns the current one.

2. The template#

The template has two steps. The invoice is a daily sales invoice to a walk-in customer, one line per VAT rate. VAT comes from the tax code, never from the line, so the VAT return picks it up like any other invoice. The journal entry books tips and the cash difference on ledgers (a journal entry line is a ledger line; lines on a relation or an invoice are not supported yet).

apiVersion: finbaze.com/flows/v1
kind: FlowTemplate
metadata: { key: pos-z-report, version: 1, recordType: z_report }
chart: rgs
ledgers:
  till_cash: "<ledger code of the till cash account>"
on:
  - transition: { to: closed }
    onDataChange: rebook
    steps:
      - id: customer
        upsert_relation:
          externalId: pos-walk-in
          legalName: Walk-in customers
      - id: sales
        create_sales_invoice:
          relation: "= steps.customer.id"
          date: "= $date(record.data.closedAt, 'Europe/Amsterdam')"
          currency: EUR
          reference: "= 'Z ' & record.data.till"
          finalise: true
          lines:
            - forEach: "= record.data.sales"
              as: row
              name: "= row.name"
              quantity: 1
              priceMinor: "= $net(row.grossMinor, row.taxCode)"
              taxCode: "= row.taxCode"
      - id: cash_difference
        when: "= record.data.cashDifferenceMinor != 0"
        create_journal_entry:
          description: "= 'Cash difference ' & record.data.till"
          date: "= $date(record.data.closedAt, 'Europe/Amsterdam')"
          lines:
            - { ledger: "{{ ledgers.cash_difference }}", amountMinor: "= -record.data.cashDifferenceMinor" }
            - { ledger: "{{ ledgers.till_cash }}", amountMinor: "= record.data.cashDifferenceMinor" }

$net(grossMinor, taxCode) turns a gross amount into net with the tax code's rate. onDataChange: rebook reverses the earlier bookings and books again when a closed report is corrected. Test it without writing anything:

mutation Test($source: String!, $records: [JSON!]!) {
  testFlowTemplate(source: $source, format: yaml, records: $records) {
    valid
    errors { code message path }
    runs { recordExternalId trigger plannedObjects { stepId objectType action externalId } error { code message } }
  }
}

Then saveFlowTemplate (saving the same text again returns the same version) and activateFlowTemplate(key: "pos-z-report", version: <the version it returned>). Activation checks that the ledgers named in ledgers exist in the companies' charts.

The POS demo ships the first two steps (customer and sales); add cash_difference once you know your ledger codes.

3. Push the Z-report#

mutation Z($company: JSON!, $data: JSON!) {
  upsertCustomRecord(input: { company: $company, type: "z_report", externalId: "till-1-2026-03-02", status: "closed", data: $data }) {
    status
    record { id revision effects { stepId objectType } }
  }
}

with data like { "till": "1", "closedAt": "2026-03-02T22:40:00Z", "sales": [{ "name": "Food", "grossMinor": 52500, "taxCode": "NL_2019_LOW" }, { "name": "Drinks", "grossMinor": 31000, "taxCode": "NL_2012_HIGH" }], "cashDifferenceMinor": -250 }. The record id is yours, so pushing again changes nothing; a changed data bumps revision.

  • Tips are not revenue. Book them in a separate journal entry step on {{ ledgers.tips }} if your policy names one; do not add them to a sales line.
  • One run is one transaction: if a step fails nothing is kept and the run is failed. Runs of one record go in order, so fix the template or data and retryFlowRun. Events: flow_run.succeeded, flow_run.failed.

4. Settlements and the VAT return#

The card settlement shows up in the bank. Push it with the data source flow, or let the company connect its bank. Mount vat-return and purchase-inbox for the VAT return and incoming purchase invoices:

<FinBazeVatReturn period="current" />
<FinBazePurchaseInbox tab="inbox" />

Create the session with components: { vatReturn: { enabled: true }, purchaseInbox: { enabled: true } }. The VAT return needs the company's filing consent (Filing consent).

Try it in the sandbox#

seedTestCompany(input: { template: nl_horeca_pos }) gives a hospitality sole trader with food at the low rate, drinks at the high rate and daily card settlements. Push a Z-report, check plannedObjects with testFlowTemplate, then advance a test clock to the end of the quarter and open vat-return.

Pitfalls#

  • Sending VAT amounts on lines: tax is derived from the tax code.
  • Correcting a closed report by pushing a new report: update the same externalId instead, so the flow rebooks.
  • Using a ledger code the company's chart does not have: activation fails; run activateFlowTemplate before go-live.