aliziodev/payid-transactions

Payment transaction ledger and webhook event store for PayID ecosystem.

Maintainers

Package info

github.com/aliziodev/payid-transactions

pkg:composer/aliziodev/payid-transactions

Statistics

Installs: 8

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v0.1.0 2026-04-13 18:36 UTC

This package is auto-updated.

Last update: 2026-04-13 18:37:19 UTC


README

Latest Version on Packagist Total Downloads PHP Version Laravel Version

Shared payment ledger package for PayID ecosystem.

Requirements

  • PHP ^8.3
  • Laravel ^12.0|^13.0 (illuminate/database, illuminate/support)

CI and Release

  • CI workflow runs on push/PR to main:
    • composer validate --strict
    • composer lint-check
    • composer test
  • Auto tag release workflow reads version from composer.json at:
    • extra.release.version
  • On push to main (when composer.json changes), the workflow:
    • creates git tag v<version> if not exists
    • creates GitHub Release with generated notes

Release steps for maintainers:

  1. Bump extra.release.version in composer.json.
  2. Merge/push to main.
  3. Workflow creates tag and GitHub release automatically.

Scope

  • Persist payment transaction lifecycle (payment_transactions).
  • Persist webhook delivery and processing trail (payment_webhook_events).
  • Provide service contract for recording and updating transaction state.

Schema Notes

  • Transaction identity is composite: provider + merchant_order_id.
  • If idempotency_key is provided, transaction upsert identity becomes provider + idempotency_key.
  • Optional polymorphic linkage is available via subject_type + subject_id.
  • Webhook table stores replay-safe event_fingerprint and processing audit fields.

Terminology Glossary

This package uses gateway-neutral naming so one schema can serve multiple providers.

  • provider: payment gateway identifier (midtrans, stripe, xendit, doku, paddle, etc).
  • merchant_order_id: merchant-side business reference for the payment intent. Common equivalents in other systems: external_id, invoice_number, order_code.
  • provider_transaction_id: gateway-side transaction/reference ID (pi_..., txn_..., etc).
  • idempotency_key: client/service generated deduplication key for retry-safe writes.
  • subject_type and subject_id: domain link (similar to polymorphic relation by convention). Examples: order, subscription, marketplace_order, wallet_topup.
  • event_fingerprint: deterministic unique value to detect webhook replay/retry.

Naming Guidelines

Recommended conventions for production teams:

  • Keep provider lowercase slug and stable (stripe, not Stripe/STRIPE).
  • Keep merchant_order_id immutable after first successful write.
  • Use idempotency_key for all external-call retries (charge, confirm, capture, etc).
  • Use consistent subject_type vocabulary across services (document in one place).
  • Put gateway-specific extras in metadata, keep core columns provider-agnostic.

Gateway Field Mapping (Practical)

  • Midtrans: merchant_order_id <= order_id, provider_transaction_id <= transaction_id.
  • Stripe: merchant_order_id <= invoice/external order reference, provider_transaction_id <= payment_intent/charge ID.
  • Xendit: merchant_order_id <= external_id, provider_transaction_id <= payment/invoice transaction ID.
  • DOKU: merchant_order_id <= merchant invoice/order number, provider_transaction_id <= DOKU transaction reference.
  • Paddle: merchant_order_id <= merchant checkout/invoice reference, provider_transaction_id <= Paddle transaction ID.

Non-scope

  • Invoice domain model.
  • Subscription orchestration.
  • Provider API adapter logic.

Install

composer require aliziodev/payid-transactions

Publish

php artisan vendor:publish --tag=payid-transactions-config
php artisan vendor:publish --tag=payid-transactions-migrations

Usage

Resolve ledger service from container:

$ledger = app(\Aliziodev\PayIdTransactions\Contracts\TransactionLedger::class);

$ledger->upsertStatus([
    'provider' => 'midtrans',
    'merchant_order_id' => 'ORDER-1001',
    'status' => 'paid',
    'amount' => 100000,
    'currency' => 'IDR',
    'subject_type' => 'subscription',
    'subject_id' => '01JABCDEF...',
]);

Prune old webhook audit rows:

php artisan payid-transactions:prune-webhooks --days=90

Real-World Scenarios

The same ledger structure can be used across local and global gateways for ecommerce, subscription billing, marketplace payouts, and digital products.

1) Midtrans - ecommerce checkout (one-time payment)

$ledger->recordChargeAttempt([
    'provider' => 'midtrans',
    'merchant_order_id' => 'ECOM-20260413-0001',
    'idempotency_key' => 'checkout-user-123-cart-888-v1',
    'status' => 'pending',
    'amount' => 350000,
    'currency' => 'IDR',
    'subject_type' => 'order',
    'subject_id' => 'ORD-01JXYZ123',
    'customer_reference' => 'user123@example.com',
    'metadata' => [
        'channel' => 'qris',
        'cart_id' => 'CART-888',
    ],
]);

$ledger->upsertStatus([
    'provider' => 'midtrans',
    'merchant_order_id' => 'ECOM-20260413-0001',
    'provider_transaction_id' => 'trx-9f3c1',
    'status' => 'paid',
    'amount' => 350000,
    'currency' => 'IDR',
    'subject_type' => 'order',
    'subject_id' => 'ORD-01JXYZ123',
]);

2) Stripe - SaaS subscription renewal

$ledger->recordChargeAttempt([
    'provider' => 'stripe',
    'merchant_order_id' => 'INV-2026-05-ACME-01',
    'idempotency_key' => 'stripe-sub-renew-sub_01ABC-2026-05',
    'status' => 'pending',
    'amount' => 199900,
    'currency' => 'USD',
    'subject_type' => 'subscription',
    'subject_id' => 'SUB-01ABC',
    'customer_reference' => 'cus_Nx12ABC',
    'metadata' => [
        'invoice_id' => 'in_1Px...',
        'billing_cycle' => '2026-05',
    ],
]);

$ledger->upsertStatus([
    'provider' => 'stripe',
    'merchant_order_id' => 'INV-2026-05-ACME-01',
    'provider_transaction_id' => 'pi_3Qx...',
    'status' => 'paid',
    'amount' => 199900,
    'currency' => 'USD',
    'subject_type' => 'subscription',
    'subject_id' => 'SUB-01ABC',
]);

3) Paddle - digital product/license sale

$ledger->recordChargeAttempt([
    'provider' => 'paddle',
    'merchant_order_id' => 'LIC-2026-0042',
    'idempotency_key' => 'paddle-checkout-ctm_778-prod_42',
    'status' => 'pending',
    'amount' => 4900,
    'currency' => 'USD',
    'subject_type' => 'license_order',
    'subject_id' => 'LIC-ORD-42',
    'customer_reference' => 'ctm_778',
    'metadata' => [
        'product_id' => 'pro_plan',
        'license_type' => 'lifetime',
    ],
]);

$ledger->upsertStatus([
    'provider' => 'paddle',
    'merchant_order_id' => 'LIC-2026-0042',
    'provider_transaction_id' => 'txn_01hxyz...',
    'status' => 'paid',
    'amount' => 4900,
    'currency' => 'USD',
    'subject_type' => 'license_order',
    'subject_id' => 'LIC-ORD-42',
]);

4) DOKU - local VA payment

$ledger->recordChargeAttempt([
    'provider' => 'doku',
    'merchant_order_id' => 'DOKU-ORDER-0099',
    'idempotency_key' => 'doku-va-order-99-v1',
    'status' => 'pending',
    'amount' => 275000,
    'currency' => 'IDR',
    'subject_type' => 'order',
    'subject_id' => 'ORD-0099',
    'customer_reference' => 'customer-0099',
    'metadata' => [
        'channel' => 'va_bni',
        'store' => 'jakarta-01',
    ],
]);

$ledger->upsertStatus([
    'provider' => 'doku',
    'merchant_order_id' => 'DOKU-ORDER-0099',
    'provider_transaction_id' => 'DOKU-TXN-7788',
    'status' => 'paid',
    'amount' => 275000,
    'currency' => 'IDR',
    'subject_type' => 'order',
    'subject_id' => 'ORD-0099',
]);

5) Xendit - online store with invoice lifecycle

$ledger->recordChargeAttempt([
    'provider' => 'xendit',
    'merchant_order_id' => 'XND-INV-2026-01',
    'idempotency_key' => 'xendit-invoice-ext_123-v1',
    'status' => 'pending',
    'amount' => 850000,
    'currency' => 'IDR',
    'subject_type' => 'order',
    'subject_id' => 'ORD-7788',
    'customer_reference' => 'customer@example.com',
    'metadata' => [
        'invoice_id' => 'inv-123',
        'payment_method' => 'ewallet',
    ],
]);

$ledger->upsertStatus([
    'provider' => 'xendit',
    'merchant_order_id' => 'XND-INV-2026-01',
    'provider_transaction_id' => 'pay-abc-001',
    'status' => 'paid',
    'amount' => 850000,
    'currency' => 'IDR',
    'subject_type' => 'order',
    'subject_id' => 'ORD-7788',
]);

Webhook Audit Example

Use webhook event store for retries/replay visibility and processing outcomes:

$event = $ledger->recordWebhookEvent([
    'provider' => 'stripe',
    'event_fingerprint' => hash('sha256', 'stripe|evt_123|pi_123'),
    'external_event_id' => 'evt_123',
    'merchant_order_id' => 'INV-2026-05-ACME-01',
    'provider_transaction_id' => 'pi_3Qx...',
    'signature_valid' => true,
    'payload' => ['type' => 'invoice.paid'],
    'received_at' => now(),
]);

$ledger->markWebhookProcessed($event, true);

Suggested Subject Mapping

  • ecommerce order: subject_type = order, subject_id = order_id
  • subscription billing: subject_type = subscription, subject_id = subscription_id
  • marketplace order: subject_type = marketplace_order, subject_id = marketplace_order_id
  • digital goods/license: subject_type = license_order, subject_id = license_order_id
  • wallet top-up: subject_type = wallet_topup, subject_id = topup_id