Skip to main content
Complete work archive

Production / Payment wallet and marketplace checkout

Epayindo

A Laravel payment wallet connected to the Winpay gateway, and a digital marketplace whose checkout ran through it.

Relationship
Freelance · PT Ciberindo Makmur Sejahtera
Role
Backend Engineer
Period
Dec 2019 – Jan 2020

Summary

Freelance work for PT Ciberindo Makmur Sejahtera, concurrent with my job at Java Multi Mandiri, across two Laravel applications built from purchased scripts: Epayindo, a payment wallet, and RumahPro, a digital marketplace. In Epayindo I added the Winpay gateway: payment requests that stored Winpay's status and listener URLs against the transaction, and a notification endpoint that logged each callback, forwarded the outcome to the merchant's notification URL, and credited the wallet only if the transaction was not already paid. In RumahPro I built the checkout that signed in to the Epayindo API, created the transaction, saved its ID on the order, and sent the customer the payment link by WhatsApp and email.

At a glance

  • Freelance work, concurrent with my job at Java Multi Mandiri
  • Both applications started from purchased scripts; the gateway work and the integration between them were mine
  • Logged every Winpay notification before acting on it, then forwarded the outcome to the merchant's notification URL
  • Credited the wallet only when a successful notification found the transaction unpaid, so a repeated callback did not credit twice
  • RumahPro checkout created an Epayindo transaction, stored its ID on the order, and sent the payment link by WhatsApp and email

Stack: PHP, Laravel, MySQL, REST APIs, Winpay, WhatsApp API

Read the full case study2 stories · ~3 min read

Inputs

Winpay notifications
Marketplace orders
Payment wallet and marketplace checkout

Outputs

Wallet credits
Merchant notifications
Payment links
What this system takes in, and what reads from it.

System context

Epayindo was a Laravel payment wallet: customers and merchants held balances, and merchants could take payments through it. RumahPro was a Laravel digital marketplace operated by the same client. Both started from purchased scripts, and neither script spoke to an Indonesian payment gateway or to the other application.

PT Ciberindo Makmur Sejahtera engaged me as a freelancer from December 2019 to January 2020, concurrent with my employment at Java Multi Mandiri. My work sat on the payment path: connecting Epayindo to the Winpay gateway, and making RumahPro's checkout create and track its payments through Epayindo.

My scope

I owned

  • The Winpay payment request in Epayindo, storing Winpay's status and listener URLs against the transaction.
  • The Winpay notification endpoint: logging, forwarding the outcome to the merchant, and settling the transaction and wallet balance.
  • The RumahPro checkout API that authenticated against Epayindo, created the transaction, and linked it to the marketplace order.
  • The RumahPro invoice event and its WhatsApp and email notifications carrying the Epayindo payment link.

Team-owned

The wallet, merchant, and transaction model came from Epayindo's purchased script, and the catalogue, cart, and accounts from RumahPro's. The WhatsApp delivery API was a third-party service. Product direction and operations belonged to the client.

Key engineering work

Settling Winpay notifications into the wallet

Payment gateway

Problem

Winpay reported a payment's outcome by calling back to Epayindo. That callback was the only signal that money had moved, and it had to update three parties: the transaction, the customer's wallet, and the merchant whose order was waiting on the result.

Action

The notification endpoint recorded every inbound callback, with its URL and full body, before doing anything else. It then found the transaction by the ID Winpay sent back. On a success code it forwarded the transaction to the merchant's notification URL, when the payment belonged to a merchant, and logged the merchant's response alongside it. It changed the transaction to paid and credited the wallet only if the transaction was not already paid, then sent the payment confirmation by email and WhatsApp. On any other code it still forwarded the transaction, so a merchant learned about failures as well as successes.

Result

Each callback left an inbound record, and each forward an outbound record with the merchant's response. A repeated success callback for a paid transaction was forwarded again but did not credit the wallet a second time.

Taking marketplace checkout through the wallet

Integration

Problem

RumahPro needed its orders paid through Epayindo rather than its script's built-in payment options, and customers needed a way to reach the payment page after checkout.

Action

I added a checkout API to RumahPro that validated the customer's email, phone, and items, created the marketplace order as pending, and signed in to the Epayindo API as a merchant, reusing the session token on later calls. It then created the Epayindo transaction and saved the returned transaction ID on the order, so either system's record could be traced to the other. For signed-in customers, order creation raised an invoice event whose listeners sent the invoice by email and the Epayindo payment link by WhatsApp. A transaction endpoint let RumahPro read the current state of a payment back from Epayindo.

Result

RumahPro orders went live paid through Epayindo payment links, with the Epayindo transaction ID stored on each order.

Technical decisions

Each application kept its own records and referred to the other by ID. RumahPro did not store payment state of its own beyond the Epayindo transaction ID; it asked Epayindo when it needed the current state. That kept a single source of truth for whether an order had been paid, at the cost of an API call when the state was needed.

Looking back, two things are worth changing. The notification endpoint trusted Winpay's response code without verifying a signature, and the paid check and the wallet credit were not wrapped in a transaction or a row lock, so two simultaneous callbacks could both pass the check. Later payment work, such as the MySQL locks on Funraisin's matched-giving path, is where I closed exactly that kind of gap.