Schedule icon
Write icon
Queries icon
If icon
Log icon
SlackIncomingWebhook icon

Close the Day at a Retail Store by Reconciling UPI Payments and Cash Drawers

Reconcile POS bills with UPI settlements and cash drawer counts nightly. Catch fake payment screens, reused UPI references, short payments and cash gaps.

Categories
BusinessData

In Indian retail most bills are now paid by UPI, and the cashier usually closes the bill after looking at the customer's phone. A payment that failed, a screen that was edited, or one payment screenshot shown for two bills all look like a paid bill at the counter, and the gap only shows up days later when someone compares the settlement report by hand. This blueprint closes the day automatically: it matches every UPI bill to the payment provider's settlement report, reconciles each counter's cash drawer, and lists exactly which bills and counters to look at. Everything runs locally in DuckDB: no payment gateway credentials and no sales data leaving your Kestra instance.

How it works

  1. sample_pos_bills, sample_upi_settlement and sample_cash_count (io.kestra.plugin.core.storage.Write) write one day of data for two counters with one example of every problem. They are used only when the matching input is empty, so the flow runs out of the box.

  2. reconcile (io.kestra.plugin.jdbc.duckdb.Queries) loads the three CSV files through inputFiles, normalises UPI references (removes spaces) and runs these checks:

    • UPI_NOT_RECEIVED (error): the bill's UPI reference is missing from the settlement report, or the payment is FAILED or PENDING;
    • DUPLICATE_UPI_REF (error): the same UPI reference is recorded on more than one bill;
    • UPI_AMOUNT_MISMATCH (error): the payment was received but for a different amount than the bill;
    • UPI_REF_MISSING (warning): a UPI bill has no reference; a successful payment of the same amount on the same counter within match_window_minutes is suggested as its match (an error when none or several fit);
    • UPI_WITHOUT_BILL (warning): money was received but no bill claims it (a sale that was not billed, or a bill cancelled after payment);
    • CASH_SHORT (error), CASH_OVER (warning) and CASH_NOT_COUNTED (error): counted cash versus opening float plus cash sales (refunds are negative cash bills), beyond cash_tolerance.

    The exceptions and a per-counter summary are written to CSV with outputFiles, and the final SELECT returns the totals through fetchType: FETCH_ONE, available as outputs.reconcile.outputs[0].row.

  3. route_findings (io.kestra.plugin.core.flow.If) logs every exception when there is at least one error, and a nested If posts the same findings to Slack when notify_slack is true. A clean close is logged as well, so the execution history doubles as the store's daily close register.

  4. The errors block logs a failed close loudly, because a close that silently does not run looks the same as a clean day.

  5. The nightly_close Schedule trigger (23:30 IST, shipped disabled) runs the close after the store shuts.

What you get

  • outputs.exceptions: CSV with severity, check_name, terminal, bill_id, upi_reference, billed, received and detail, errors first.
  • outputs.counter_summary: CSV with one row per counter: bills, cash_sales, upi_billed, card_sales, upi_received, opening_float, expected_cash and counted_cash.
  • outputs.close_summary: JSON with total_bills, total_sales, upi_billed, upi_received, cash_difference, error_count, warning_count, error_details and warning_details.
  • With the sample data: 6 errors (a FAILED payment closed as paid, a reference that never reached the account, one reference used on two bills, a Rs 54 short payment, and a counter Rs 200 short in cash) and 2 warnings (Rs 820 received with no bill, and a bill without a reference that is matched by amount and time). A reference typed with spaces is normalised and matches, and a counter Rs 10 over stays within the tolerance.

Who it's for

  • Kirana stores, supermarkets, pharmacies and restaurants that take UPI at the counter and close the day by hand.
  • Finance and operations teams of multi-store retail chains that need one daily close per store and counter.
  • POS and retail software teams that want to offer an automated end-of-day close to their customers.

Why orchestrate this with Kestra

A spreadsheet can reconcile one day once. Kestra runs the close every night, keeps every day's exceptions in the execution history, branches on the result and alerts the store manager before the next shift opens. Because the inputs are plain files, the same flow can be triggered when the POS export or the settlement report lands in S3, SFTP or Google Drive, or run once per store with a ForEach.

Prerequisites

  • Nothing for the demo: the sample files are built in.
  • For your own data: three CSV exports with the columns listed in the inputs below. Most UPI payment providers and bank merchant portals let you download the day's transactions with the RRN, amount, status and terminal or VPA.

Inputs

  • pos_bills (FILE, optional): bill_id, terminal, bill_time, payment_mode (CASH, UPI or CARD), amount (negative for refunds) and upi_ref.
  • upi_settlement (FILE, optional): rrn, txn_time, amount, status (SUCCESS, FAILED or PENDING) and terminal.
  • cash_count (FILE, optional): terminal, opening_float and counted_cash.
  • cash_tolerance (FLOAT, default 50): accepted difference in rupees between counted and expected cash.
  • match_window_minutes (INT, default 10): time window used to suggest a match for bills without a UPI reference.
  • notify_slack (BOOL, default false): post the findings to Slack when the day does not close cleanly.

Secrets

  • SLACK_WEBHOOK_URL: Slack incoming webhook URL. Only needed when notify_slack is true.

Quick start

  1. Save the flow and run it with the default inputs.
  2. Open the logs of log_errors and download exceptions and counter_summary from the Outputs tab.
  3. Run it again with your own POS export, settlement report and cash count.
  4. Optional: add the SLACK_WEBHOOK_URL secret and set notify_slack to true.
  5. Point the inputs at your real sources and set disabled: false on nightly_close.

Common pitfalls

  • Map the provider's terminal ID or VPA to the same counter names your POS uses, otherwise every payment shows up as received without a bill.
  • Settlement reports can lag: a payment that is PENDING at night may succeed later. Run the close after the provider's cut-off, or re-run it the next morning for the pending ones.
  • Keep the UPI reference column as text when exporting from a spreadsheet, so the 12-digit RRN is not turned into a number in scientific notation.

How to extend

  • Read bills straight from the POS database (Postgres, MySQL) instead of a CSV export, and write exceptions back to a table.
  • Add card settlements from the card machine provider in the same way as UPI.
  • Run the close for every store with a ForEach over a store list, and open one ticket per error in Jira or ServiceNow.
  • Trigger the flow when the settlement report lands, with the S3, SFTP or Google Drive triggers.

Links

See How

New to Kestra?

Use blueprints to kickstart your first workflows.