Get Paid On Time

2026 to now SaaS, live FastAPI React PostgreSQL Celery Stripe myDATA .NET connector

Credit control for Greek businesses, live at gpot.gr. It tracks who owes what and sends the reminders. Invoices and payments come from myDATA and from the customer's ERP, then the platform reconciles those against manual entry. When those three disagree it flags the field and stops, because quietly picking a winner is how you chase someone for money they already paid.

gpot.gr

The problem

  • Credit control is who owes what, when to remind them, when to escalate. That only works if the invoice and the payment are true.
  • Three systems disagree about the same invoice: myDATA at the tax authority, the customer's ERP, and whatever someone typed in by hand.
  • Pick one source and overwrite the others and you quietly lose money: a paid invoice marked unpaid, or the wrong amount chased for a month.
  • The data also arrives slowly and in volume, and a sync that dies halfway cannot be allowed to corrupt the running total.

Key decisions and tradeoffs

  • Flag, do not overwrite. When two sources disagree, each differing field becomes a conflict row and the invoice is held at conflict_status=pending. An operator resolves it, with the account's source priority as a suggestion, never as an automatic decision.
  • Syncs are resumable. The cursor is persisted after every page of a thousand rows, each run has a wall-clock budget that stays under Celery's timeout, and when the budget runs out it enqueues a continuation. A cursor that stops advancing trips a livelock guard.
  • Upserts are idempotent, keyed on invoice number or external id, so re-running a page is safe. A payment whose invoice has not arrived yet is held and retried for a few runs, then dead-lettered with a reason rather than dropped silently.
  • One Redis lock per account and source stops two workers syncing the same tenant at once; dedicated Celery queues keep a slow ERP sync from blocking notifications.
  • The ERP connector is an anti-corruption layer. External DTOs mirror the connector exactly and mappers translate them, so a change in an ERP's schema touches one file and nothing downstream.

What's inside

  • Receivables, overdue tracking, reminder and escalation rules, billed on Stripe.
  • Pulls from myDATA and from the customer's ERP, then reconciles those against manual entry.
  • Conflicts become a held invoice for an operator, never an automatic overwrite.
  • On-prem .NET connector (API, Windows service, desktop setup) so database credentials never leave the building.
  • Resumable, idempotent syncs on dedicated queues, with per-account locks so two runs never race.
  • Encrypted credentials, TOTP, Greek and English throughout.

Shipping it

  • The ERP connector is a .NET solution: API, core, infrastructure, a Windows service and a desktop configuration app, so a customer's IT can install and configure it without me in the room.
  • That connector runs inside the customer's own network and exposes one standard API outward, so their database credentials never leave the building.
  • Containerised end to end with separate dev, staging and production compositions, nginx terminating TLS in front, and backend, workers, beat, frontend, Postgres and Redis as separate services.
  • CI builds and checks the backend, the frontend and the .NET connector independently, then builds the images.

Scale and failure modes

  • 170+ test files across the backend, the frontend and the .NET connector.
  • Three Celery queues, so a slow ERP sync cannot block reminders.
  • Live at gpot.gr, running its own billing on Stripe with usage-based overage.
  • Encrypted credentials at rest, TOTP two-factor auth, and an isolated production environment with staging parity, automated backups and tested restores.