---
title: Get Paid On Time
slug: gpot
author: Charis Nikolaidis
year: 2026 to now
type: SaaS, live
tech: [FastAPI, React, PostgreSQL, Celery, Stripe, myDATA, .NET connector]
url: https://charisnikolaidis.com/projects/gpot/
live: https://gpot.gr
---

# Get Paid On Time

Credit control for Greek businesses, live at [gpot.gr](https://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.

**Type:** SaaS, live · **Period:** 2026 to now
**Stack:** FastAPI, React, PostgreSQL, Celery, Stripe, myDATA, .NET connector
**Live:** <https://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.

## Related

- Profile: <https://charisnikolaidis.com/llms.txt>
- All projects: <https://charisnikolaidis.com/projects/>
- Queryable over MCP: `https://charisnikolaidis.com/mcp`
