Kembali ke katalog

STUDI KASUS · DEMO DATA SINTETIS

Pesanan dan pembayaran terpisah.
Selisihnya terlihat jelas.

Contoh alur rekonsiliasi untuk bisnis yang menerima banyak transaksi: dua file diproses menjadi daftar pesanan cocok, belum dibayar, dibayar ganda, dan nominal berbeda.

Ini demonstrasi, bukan hasil proyek klien. Semua identitas dan transaksi dibuat secara sintetis. Angka di bawah berasal dari skrip yang dapat dijalankan ulang; tidak mewakili performa atau hasil bisnis nyata.

01 / MASALAH

File ada, gambaran transaksi belum utuh.

Tim operasional memegang daftar pesanan dan laporan pembayaran yang terpisah. Membandingkan baris satu per satu menyulitkan saat jumlah transaksi bertambah, terutama ketika ada pesanan tanpa pembayaran, lebih dari satu pembayaran berhasil, atau jumlah uang yang tidak sesuai.

Demo ini menjawab pertanyaan sederhana: pesanan mana yang sudah beres, dan mana yang perlu diperiksa manusia?

02 / SUMBER & ALUR

Dari CSV menuju daftar tindak lanjut.

Data pesananorder_id, tanggal, pelanggan samaran, nominal tagihan
Data pembayaranpayment_id, order_id, waktu, nominal, status, referensi penyedia
Aturan pencocokanGabungkan berdasarkan order_id, hitung pembayaran berhasil, bandingkan nominal
Ringkasan & detailJumlah per kategori dan daftar transaksi yang perlu ditindaklanjuti
01Belum dibayar

Tidak ditemukan pembayaran berstatus berhasil untuk order_id tersebut.

02Dibayar ganda

Ada lebih dari satu pembayaran berhasil yang berbeda untuk satu pesanan.

03Nominal berbeda

Satu pembayaran berhasil ditemukan, tetapi jumlahnya tidak sama dengan tagihan.

04Cocok

Tepat satu pembayaran berhasil dengan jumlah yang sama.

03 / HASIL DEMO

Contoh tampilan dashboard.

10.000 pesanan sintetis · 9.950 baris pembayaran · periode simulasi September 2026

Ringkasan rekonsiliasiDATA SINTETIS
Transaksi cocok9.300dari 10.000 pesanan
Belum dibayar300dari 10.000 pesanan
Dibayar ganda200dari 10.000 pesanan
Nominal berbeda200dari 10.000 pesanan
Contoh empat baris hasil rekonsiliasi
ID pesananTagihanTerbayarPembayaranStatus
ORD-00001Rp57.919Rp57.9191Cocok
ORD-09301Rp554.619Rp00Belum dibayar
ORD-09601Rp80.319Rp160.6382Dibayar ganda
ORD-09801Rp714.119Rp724.1191Nominal berbeda

Selain kategori pesanan di atas, terdeteksi 50 pembayaran berhasil tanpa pesanan yang cocok. Ini dilaporkan terpisah agar tidak mengubah total 10.000 pesanan.

COBA SENDIRI · 8 PESANAN SINTETIS

Ubah pembayaran, lihat statusnya.

Atur jumlah pembayaran berhasil dan total terbayar. Hasil dihitung di perangkat Anda; contoh ini tidak mengirim data ke server.

Cocok5
Belum dibayar1
Dibayar ganda1
Nominal berbeda1
8 dari 8 pesanan
Pesanan contoh; ubah nilai untuk mencoba aturan rekonsiliasi
ID pesananTagihanPembayaran berhasilTotal terbayar (Rp)Status
ORD-DEMO-01Rp 125.000Cocok
ORD-DEMO-02Rp 82.000Cocok
ORD-DEMO-03Rp 250.000Belum dibayar
ORD-DEMO-04Rp 95.000Dibayar ganda
ORD-DEMO-05Rp 175.000Nominal berbeda
ORD-DEMO-06Rp 45.000Cocok
ORD-DEMO-07Rp 320.000Cocok
ORD-DEMO-08Rp 69.000Cocok

Urutan aturan: 0 pembayaran → belum dibayar; lebih dari 1 → dibayar ganda; 1 pembayaran → nominal dibandingkan. Ini demo sederhana, belum mencakup refund, cicilan, settlement, dan biaya penyedia pembayaran. Skrip Python di bawah dapat digunakan untuk menghasilkan file CSV lengkap.

DEMO LANJUTAN

Ingin mencoba dari dua file CSV?

Ubah data pesanan dan pembayaran contoh, jalankan validasi, lalu telusuri alasan setiap status dan pembayaran tanpa pesanan.

Coba demo CSV

Angka dan contoh ID di halaman ini dihasilkan oleh skrip Python di bawah. Salin sebagai rekonsiliasi.py, lalu jalankan python3 rekonsiliasi.py --output demo/output untuk membuat CSV pesanan, pembayaran, hasil rekonsiliasi, dan ringkasannya. Untuk implementasi klien, aturan refund, cicilan, biaya penyedia pembayaran, batas waktu, dan data settlement harus disepakati lebih dahulu.

Lihat dan salin kode demo lengkap
"""Reproducible synthetic order/payment reconciliation demo for NAFAF.

Run: python3 rekonsiliasi.py --output demo/output
No external packages or customer data are required.
"""

import argparse
import csv
import json
from collections import Counter, defaultdict
from datetime import datetime, timedelta
from pathlib import Path


def write_csv(path, rows, fields):
    with path.open("w", newline="", encoding="utf-8") as handle:
        writer = csv.DictWriter(handle, fieldnames=fields)
        writer.writeheader()
        writer.writerows(rows)


def read_csv(path):
    with path.open(newline="", encoding="utf-8") as handle:
        return list(csv.DictReader(handle))


def generate(output):
    orders = []
    payments = []
    day = datetime(2026, 9, 1, 9, 0)

    def add_payment(order_id, amount):
        index = len(payments) + 1
        payments.append({
            "payment_id": f"PAY-{index:05d}",
            "order_id": order_id,
            "paid_at": (day + timedelta(minutes=index)).isoformat(timespec="minutes"),
            "amount_idr": amount,
            "status": "SUCCESS",
            "provider_reference": f"REF-{index:06d}",
        })

    for index in range(1, 10001):
        order_id = f"ORD-{index:05d}"
        amount = 50000 + (index * 7919) % 950000
        orders.append({
            "order_id": order_id,
            "order_date": (day + timedelta(minutes=index)).isoformat(timespec="minutes"),
            "customer_code": f"CUS-{1 + index % 750:04d}",
            "amount_idr": amount,
        })
        if index <= 9300:
            add_payment(order_id, amount)
        elif index <= 9600:
            continue  # No successful payment.
        elif index <= 9800:
            add_payment(order_id, amount)
            add_payment(order_id, amount)  # Two distinct successful captures.
        else:
            add_payment(order_id, amount + 10000)

    for index in range(1, 51):
        add_payment(f"ORD-X{index:04d}", 75000 + index * 1000)

    write_csv(output / "orders.csv", orders, ["order_id", "order_date", "customer_code", "amount_idr"])
    write_csv(output / "payments.csv", payments, ["payment_id", "order_id", "paid_at", "amount_idr", "status", "provider_reference"])


def reconcile(output):
    orders = read_csv(output / "orders.csv")
    payments = read_csv(output / "payments.csv")
    by_order = defaultdict(list)
    for payment in payments:
        if payment["status"] == "SUCCESS":
            by_order[payment["order_id"]].append(payment)

    results = []
    for order in orders:
        matched = by_order.pop(order["order_id"], [])
        expected = int(order["amount_idr"])
        paid = sum(int(payment["amount_idr"]) for payment in matched)
        if len(matched) == 0:
            status = "BELUM_DIBAYAR"
        elif len(matched) > 1:
            status = "DIBAYAR_GANDA"
        elif paid != expected:
            status = "NOMINAL_BERBEDA"
        else:
            status = "COCOK"
        results.append({
            "order_id": order["order_id"],
            "expected_idr": expected,
            "successful_payments": len(matched),
            "paid_idr": paid,
            "difference_idr": paid - expected,
            "status": status,
            "payment_ids": "|".join(payment["payment_id"] for payment in matched),
        })

    unmatched = [payment for matches in by_order.values() for payment in matches]
    write_csv(output / "reconciliation.csv", results,
              ["order_id", "expected_idr", "successful_payments", "paid_idr", "difference_idr", "status", "payment_ids"])
    write_csv(output / "unmatched_payments.csv", unmatched,
              ["payment_id", "order_id", "paid_at", "amount_idr", "status", "provider_reference"])
    summary = {
        "dataset": "SYNTHETIC_DEMO",
        "orders": len(orders),
        "payment_rows": len(payments),
        "status_counts": dict(sorted(Counter(row["status"] for row in results).items())),
        "payments_without_order": len(unmatched),
    }
    (output / "summary.json").write_text(json.dumps(summary, indent=2) + "\n", encoding="utf-8")
    return summary


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--output", type=Path, default=Path("demo/output"))
    args = parser.parse_args()
    args.output.mkdir(parents=True, exist_ok=True)
    generate(args.output)
    print(json.dumps(reconcile(args.output), indent=2))


if __name__ == "__main__":
    main()

04 / PENERAPAN

Mulai dari datanya, lalu operasionalnya.

Pada proyek nyata, contoh alur ini dapat diterapkan dengan Apache Hop untuk mengambil dan menjadwalkan data, PostgreSQL untuk penyimpanan dan pemeriksaan, dbt Core atau SQL untuk aturan rekonsiliasi, serta Metabase untuk dashboard.

  • Pemetaan kolom dan aturan status yang disetujui
  • Pipeline yang dapat dijalankan ulang tanpa menggandakan data
  • Dashboard ringkasan dan daftar pengecualian
  • Dokumentasi, hak akses, dan prosedur tindak lanjut

Demo ini mencakup pesanan dan pembayaran. Rekonsiliasi settlement bank atau penyedia pembayaran merupakan tahap berikutnya dengan data dan aturan tambahan.

BAHAS KEBUTUHAN ANDA

File transaksi Anda masih perlu diperiksa manual?

Ceritakan sumber data, jumlah transaksi, dan masalah yang sering muncul. Kami bantu susun ruang lingkup yang sesuai.

Diskusikan kebutuhan