Skip to content
clariant @ arch : ~/clariant/work/dewacloud-billing
cd ~/work

~/work/dewacloud-billing on ⎇ main [✓]❯cat case-study.md

shipping

Dewacloud Billing

Dewacloud's payments platform. Two-way integration with an undocumented billing protocol, postpaid invoicing with dunning, three payment rails, Indonesian withholding tax, card-on-file auto-pay, and server-side ad attribution, all on the company's revenue path.

role
AI Engineer Intern
period
Jun 2026 – present
company
Dewaweb
status
shipping
Next.js Prisma PostgreSQL TypeScript Vitest GitLab CI

Context

Dewacloud runs on Virtuozzo (Jelastic), whose JBilling component stays the source of truth for metering, balances, and account lifecycle. The billing system I built owns everything around it: invoices, payments, and the customer billing portal. It sits between JBilling and the payment providers, so every bug here is either lost money or an angry customer.

I’m the lead developer and build it alongside one teammate, working with AI coding agents (Claude Code and Codex) throughout.

What I built

  • Integration model, verified before building. A Phase 0 spike against the live Jelastic API overturned our starting assumption: credit is pushed by JBilling after it receives an order event, not pulled by us. That decided the architecture before any feature code existed.
  • An undocumented protocol, reverse-engineered. JBilling’s real wire format didn’t match the Virtuozzo docs. I logged failed auth attempts (username only, never the password), then added temporary capture of unauthenticated requests. That showed three differences: the secret arrives as a query parameter rather than a header, commands are dispatched via ?action=, and payloads follow the CEBI dialect. I ported the dialect from a reference integration module and locked the real format into tests so it can’t silently regress.
  • Postpaid lifecycle. Invoice emails, 7-day due dates with daily reminders, admin extend and retry, direct payment links, and self-serve prepaid-to-postpaid upgrades.
  • Three payment rails. Xendit, Duitku, and manual bank transfer. By the end of July all postpaid billing ran through Duitku, and I updated the docs to match when Xendit was retired.
  • Indonesian tax compliance. For PPh 23, the amount received is smaller than the amount invoiced. Rather than add error handling for the resulting inconsistent state, I changed the order of operations: funding to JBilling is held until the withholding slip arrives, so that state can’t happen in the first place.
  • Account enforcement. Live account status in the admin panel, working suspend and unsuspend, automatic suspension of overdue postpaid accounts, and automatic reactivation once payment lands.
  • Reliability. A test gate on every merge request, a health endpoint that reports stuck jobs, escalation for stuck notifications, a scheduled job that catches missed webhooks, and a payment path that never drops a paid payment when the JBilling notify call fails.
  • Migration and hand-off. Importing existing customers from the previous system (including phone numbers stored in scientific notation and duplicate client IDs within one export), a cut-over day runbook, a verified integration reference, and a test-platform runbook to use in place of staging without touching the live system.

Since August

  • Card-on-file auto-pay. I rebuilt auto-pay on Xendit’s recurring API: a provider-neutral groundwork, a saved-card charge client, a recurring charger that retries when the card is updated, and failure emails to the customer. Sandbox testing turned up constraints the docs don’t state: card saving has to be forced, SAVE sessions need an amount of exactly 0, and the token can be recovered by polling instead of waiting for the webhook. It’s merged and verified end to end in sandbox, and stays switched off in production behind a feature flag until it’s turned on.
  • Server-side ad attribution. A GA4 ecommerce funnel, first-touch UTM and gclid tracking stored at signup, and confirmed purchases sent server-side to GA4 and the Meta Conversions API. Top-ups started from inside the Jelastic dashboard used to lose their attribution; a dataLayer and session bridge ties them back to the right session. Tags and cookies are checked with Playwright browser tests against the live and staging containers.
  • Admin accountability. Super-admin and admin roles, and every verification and status change records which admin did it.
  • Billing operations. A fixed dunning cadence with repeated suspension notices, a choice of payment provider for dashboard top-ups, and expired checkouts that renew instead of dead-ending.
  • Payhunter, a related tool. I started Payhunter, an internal app for chasing overdue invoices over WhatsApp and email: the team ticks invoices, picks a template, reviews the rendered messages, and sends. I set up its deployment and the guards around the daily invoice feed. An invoice drops off the chase list once the feed stops listing it, an empty or broken feed never closes anything, and a reappearing invoice reopens, so nobody gets chased after paying. My teammate has led it since.

Decisions I’m glad I made

  • Not shipping auto-pay until it could charge. In July, saved-card tokens were being captured but turned out not to be chargeable. Shipping that would have promised customers something the system couldn’t do, so the UI became consent-only. The working version came later, on the recurring API, after it had been proven in sandbox.
  • Writing the blocker down first. When account-status calls failed against the Jelastic API, I documented the dead end in a draft merge request. The fix, which was cluster-level operations instead of app-level ones, landed in a follow-up that explicitly supersedes it.

Outcome

Served on its official domain since 17 July 2026. July alone was 68 merge requests on this repo, each small enough to review and revert on its own. The month ended with docs and runbooks written so someone other than me can operate it.

Stack

Next.js 16 · React 19 · Prisma 7 · PostgreSQL · TypeScript · Zod · Tailwind CSS 4 · Vitest · Docker · Caddy · GitLab CI