# Payment Journey Real Event-to-Source Map

## Purpose

This document maps `payment_journey_events` to the **real code sources that exist in this repository today**.

It is intended as an implementation handoff for integrating Journey / History writes into Laravel admin APIs and Go app APIs **without changing any business logic**.

Ownership split for this implementation:

- Laravel in this repo owns admin-side and backfill-side journey sources
- Go app APIs own customer checkout, gateway callback, renewal payment, and reconciliation-side journey sources

The rule is simple:

- keep the existing flow exactly as it is
- write journey rows as a side effect after the existing step succeeds or fails
- never let ClickHouse write failure change the original API response or workflow result

## Current Read Side

The Journey / History tab already reads from ClickHouse only.

- Route: `GET /admin/api/subscriber-360/{customer}/journey/get-listing`
- File: `routes/api.php`
- Handler: `App\Http\Controllers\Subscriber360\Subscriber360ApiController::journeyListing`
- Reader service: `App\Services\Subscriber360\SubscriberJourneyClickhouseService::listSubscriberJourney`

Current read-side files:

- `routes/api.php`
- `app/Http/Controllers/Subscriber360/Subscriber360ApiController.php`
- `app/Services/Subscriber360/SubscriberJourneyClickhouseService.php`
- `resources/views/filament/resources/customer-resource/pages/view-customer.blade.php`

## Current ClickHouse Event Contract

The Journey UI already understands these event families:

- `checkout_*`
- `package_selected`
- `coupon_*`
- `voucher_*`
- `payment_*`
- `subscription_*`
- `auto_renew_*`
- `renewal_payment_*`
- `refund_*`
- `webhook_*`
- `reconciliation_*`

These are classified in:

- `App\Services\Subscriber360\SubscriberJourneyClickhouseService::classifyEventGroup`
- `App\Services\Subscriber360\SubscriberJourneyClickhouseService::buildDescription`
- `App\Services\Subscriber360\SubscriberJourneyClickhouseService::statusMeta`

## What Is Actually Present In This Repo

As of **August 5, 2026**, this repository contains real source points for:

- manual package assignment
- historical manual-assignment backfill
- one-subscriber backfill
- admin refund
- admin subscription cancel / auto-renew cancel
- admin/API subscription upgrade
- deferred upgrade execution

This repository does **not** currently expose a real payment checkout controller or payment webhook controller for:

- Razorpay purchase callback
- Paytm purchase callback
- Justpay purchase callback
- Cashfree purchase callback
- PhonePe purchase callback
- PayU purchase callback

That means these event families are **not sourced from this repo today**:

- `checkout_started`
- `checkout_abandoned`
- `package_selected`
- `coupon_*`
- `voucher_*`
- `payment_pending` from customer checkout
- `webhook_received` from payment gateways
- `reconciliation_started`
- `reconciliation_completed`
- live first-purchase `payment_success` from app checkout

Those must be emitted from the Go app API service or Go gateway callback service that actually owns those flows.

## Real Event-to-Source Map

### 1. Manual package assignment

Source present in repo: **Yes**

- Route: `POST /admin/users/{customerId}/manual-assign`
- File: `routes/web.php`
- Controller: `App\Http\Controllers\UserAdminActionController::manualAssignPackageAjax`
- Journey hook already present:
  - `App\Http\Controllers\UserAdminActionController::appendManualAssignmentJourney`
  - `App\Services\Subscriber360\SubscriberJourneyClickhouseService::storeManualAssignmentJourney`

Current event mapping:

- `payment_success`
- `subscription_created`
- `subscription_renewed`

When to emit:

- emit after the existing order/subscription inserts succeed
- do not emit inside the main transaction if that risks changing current behavior

Field ownership:

- `sms_order_id` from generated manual SMS order id
- `sms_subscription_id` from inserted subscription id
- `gateway_name` from selected gateway provider
- `transaction_id` / `gateway_payment_id` from `purchase_token`
- `package_name` / `sms_package_id` from selected package
- `country_code` from current `tbl_order.country`, else subscriber's last known order country, else `ZZ`

Recommended `source_module`:

- `manual_assign_package`

### 2. Historical manual-assignment backfill

Source present in repo: **Yes**

- Command: `clickhouse:backfill-manual-assignment-journey`
- File: `app/Console/Commands/BackfillManualAssignmentJourneyToClickhouse.php`

Current event mapping:

- `payment_success`
- `subscription_created`
- `subscription_renewed`

When to emit:

- only during backfill
- skip if journey rows already exist for the same `sms_order_id`

Recommended `source_module`:

- `manual_assign_package`

### 3. One-subscriber historical backfill

Source present in repo: **Yes**

- Command: `clickhouse:backfill-subscriber-journey`
- File: `app/Console/Commands/BackfillSubscriberJourneyToClickhouse.php`

Purpose:

- repair one subscriber timeline from existing `tbl_order` and `tbl_customer_subscriptions`

Use this for:

- historical reconstruction
- QA validation
- gap filling after emitter rollout

Do not use this as a live API emitter.

### 4. Admin refund flow

Source present in repo: **Yes**

- Route: `POST /admin/users/{customerId}/refund`
- File: `routes/web.php`
- Controller: `App\Http\Controllers\UserAdminActionController::refundCustomerAjax`

Supported gateways in current logic:

- `razorpay`
- `paytm`
- `justpay`

Actual gateway branches in current code:

- Paytm refund API call
- Justpay refund API call
- Razorpay refund API call

Current business behavior on success:

- update `tbl_order.status = 3`
- update `tbl_customer_subscriptions.is_cancelled = 1`
- update `tbl_customer_subscriptions.status = 3`
- update `tbl_customer_subscriptions.cancelled_at = now()`
- update `tbl_customer_subscriptions.end_date = now()`
- update `tbl_customer_subscriptions.is_refunded = 1`
- insert/update refund request row in refund table

Journey event mapping to add here:

- `refund_initiated`
  - emit immediately before gateway refund request
  - include requested amount, refund mode, payment id, order id
- `refund_success`
  - emit after successful gateway response and local DB updates
  - include refund id, gateway status, refunded amount
- `refund_failed`
  - emit in failure branch when gateway rejects or HTTP call throws
  - include `failure_code`, `failure_category = gateway`, `failure_message`
- `subscription_cancelled`
  - emit after current subscription row is marked cancelled/refunded
  - use existing subscription/order ids

Recommended `source_module`:

- `admin_refund`

Recommended notes:

- use gateway response body only in `metadata`
- do not change refund behavior, only observe it

### 5. Admin subscription cancel / auto-renew cancel

Source present in repo: **Yes**

- Route: `POST /admin/users/{customerId}/cancel-subscription`
- File: `routes/web.php`
- Controller: `App\Http\Controllers\UserAdminActionController::cancelSubscription`

Current flow types in code:

- manual/admin subscription cancellation
- Razorpay recurring cancellation
- Paytm recurring cancellation
- Justpay recurring cancellation

Current business behavior:

- for manual/admin subscriptions, current code calls `cancelManualAssignedSubscription(...)`
- for gateway recurring subscriptions, current code calls the gateway cancel endpoint

Journey event mapping to add here:

- `subscription_cancelled`
  - emit after `cancelManualAssignedSubscription(...)` succeeds
  - use manual subscription/order ids
- `auto_renew_disabled`
  - emit after successful Razorpay / Paytm / Justpay recurring cancel call
  - use the resolved recurring subscription/order ids
  - include gateway cancel id/status in `metadata`

Do not emit on:

- unsupported gateway validation failure
- "auto renew not enabled" validation failure
- "subscription id not found" validation failure

Recommended `source_module`:

- `admin_cancel_subscription`

Important note:

- `cancelSubscription` currently handles **gateway recurring cancellation**, not full purchase checkout
- that means it is the right place for `auto_renew_disabled`, not `payment_*`

### 6. Subscription upgrade API and admin upgrade

Source present in repo: **Yes**

- API route: `POST /admin/api/subscriptions/upgrade`
- Admin route: `POST /admin/packages/upgrade-subscription/ajax`
- Controller: `App\Http\Controllers\Subscription\SubscriptionUpgradeController`
- Service: `App\Services\Subscription\SubscriptionUpgradeService`

Current service behavior:

- validates idempotency and current active subscription
- records `subscription_change_history`
- records `subscription_upgrade_financials`
- calls `attemptPayment(...)`
- closes current subscription
- creates upgraded subscription
- updates financial/history status

Current payment engine inside repo:

- internal deterministic payment simulation
- records rows in `subscription_upgrade_payments`
- **not** a real gateway purchase flow yet

Journey event mapping for this existing service:

- `payment_initiated`
  - emit in `executeImmediateUpgrade(...)` and `executeDeferredHistoryId(...)`
  - only if `amount_to_charge > 0`
  - source is internal upgrade payment attempt
- `payment_success`
  - emit when `attemptPayment(...)` returns success and `amount > 0`
- `payment_failed`
  - emit when `attemptPayment(...)` returns failure
- `subscription_cancelled`
  - emit after `closeCurrentSubscription(...)`
  - this represents the old subscription being ended during upgrade
- `subscription_created`
  - emit after `createUpgradedSubscription(...)`
  - this represents the new upgraded subscription row
- `auto_renew_enabled` or `auto_renew_disabled`
  - emit after `createUpgradedSubscription(...)`
  - use the target package's `auto_renew`

Do not emit payment rows when:

- `amount_to_charge <= 0`

In that case:

- skip `payment_initiated`
- skip `payment_success`
- emit only the subscription lifecycle rows

Recommended `source_module`:

- `subscription_upgrade_api`
- `subscription_upgrade_admin`
- `subscription_upgrade_deferred`

Important note:

- current schema does **not** have `upgrade_scheduled`
- for `effective_mode = next_cycle`, do not invent a new event name inside this repo unless the UI contract is intentionally expanded
- emit journey rows only when deferred execution actually runs

### 7. Deferred upgrade execution

Source present in repo: **Yes**

- Service method: `App\Services\Subscription\SubscriptionUpgradeService::processDeferredUpgrades`
- Execution method: `executeDeferredHistoryId(...)`

Journey event mapping:

- `payment_initiated` when deferred charge starts and amount > 0
- `payment_success` when deferred charge succeeds
- `payment_failed` when deferred charge fails
- `subscription_cancelled` when old subscription is closed
- `subscription_created` when new subscription row is inserted
- `auto_renew_enabled` / `auto_renew_disabled` for the new row as applicable

Recommended `source_module`:

- `subscription_upgrade_deferred`

### 8. Go app API / gateway webhook callbacks

Source present in repo: **No payment webhook controller found**

Not found in:

- `routes/web.php`
- `routes/api.php`
- `app/Http/Controllers`

What this means:

- `webhook_received` cannot currently be emitted from this repo for payment gateways
- whichever Go API actually receives Razorpay / Paytm / Justpay / Cashfree / PhonePe / PayU callbacks must emit it there

Journey event mapping for the Go payment webhook API:

- `webhook_received`
  - emit after signature verification / authenticity check
  - fill `webhook_provider`, `webhook_event_type`, `webhook_event_id`, `webhook_received_at`

Recommended `source_module`:

- `gateway_webhook`

### 9. Go app API checkout / payment source map

Source present in repo: **No public purchase API/controller found**

Not found in this codebase:

- app/mobile checkout order creation API
- package selection API
- coupon validation/apply/remove purchase API
- voucher validation/apply/remove purchase API
- live gateway handoff API for customer checkout

What this means:

- these events should not be guessed inside admin code
- they belong in the Go app API that actually creates the customer checkout flow

Go app API event mapping:

- `checkout_started`
- `checkout_abandoned`
- `package_selected`
- `coupon_validated`
- `coupon_invalid`
- `coupon_applied`
- `coupon_removed`
- `voucher_validated`
- `voucher_invalid`
- `voucher_applied`
- `voucher_removed`
- `payment_initiated`
- `payment_pending`
- `payment_success`
- `payment_failed`
- `payment_cancelled`

Recommended `source_module` examples:

- `mobile_checkout`
- `web_checkout`
- `coupon_api`
- `voucher_api`
- `gateway_order_create`

### 10. Go reconciliation / settlement source map

Source present in repo: **No dedicated payment reconciliation job found**

What this means:

- `reconciliation_started`
- `reconciliation_completed`

should be emitted by the Go worker or Go async processor that currently does post-gateway confirmation, ledger matching, order-finalization, or async settlement confirmation.

Recommended `source_module`:

- `payment_reconciliation_worker`

## Go App API Ownership

The customer purchase lifecycle should be written from Go at the real handler or worker that already owns the step.

Recommended ownership map in Go:

- checkout create handler:
  - emit `checkout_started`
- package selection endpoint:
  - emit `package_selected`
- coupon validate/apply/remove endpoint:
  - emit `coupon_validated`, `coupon_invalid`, `coupon_applied`, `coupon_removed`
- voucher validate/apply/remove endpoint:
  - emit `voucher_validated`, `voucher_invalid`, `voucher_applied`, `voucher_removed`
- gateway order create / payment handoff handler:
  - emit `payment_initiated`
- payment status polling / callback finalization handler:
  - emit `payment_pending`, `payment_success`, `payment_failed`, `payment_cancelled`
- gateway callback endpoint:
  - emit `webhook_received`
- order finalization / reconciliation worker:
  - emit `reconciliation_started`, `reconciliation_completed`
- recurring billing / renewal worker:
  - emit `renewal_payment_initiated`, `renewal_payment_success`, `renewal_payment_failed`, `subscription_renewed`

If the Go service also turns auto-renew on or off during the recurring lifecycle, it should emit:

- `auto_renew_enabled`
- `auto_renew_disabled`

## Integration Rules By Existing Source

### Rule A: write after the real step, not instead of it

Examples:

- after `refundCustomerAjax` receives successful gateway response and updates local state
- after `cancelSubscription` gets a successful gateway cancel response
- after `manualAssignPackageAjax` inserts order and subscription
- after `createUpgradedSubscription(...)` inserts the new upgraded subscription
- after the Go checkout or webhook handler already knows the real payment outcome

### Rule B: preserve failure behavior

If the existing code returns:

- validation error
- gateway error
- unsupported gateway error
- missing subscription/payment/order error

the same response must still be returned.

Journey writing must only add logging/timeline rows, never new branching.

### Rule C: use deterministic idempotency keys

Recommended pattern:

- `manual_assign:{app_id}:{sms_order_id}:payment_success`
- `refund:{app_id}:{payment_id}:initiated`
- `refund:{app_id}:{payment_id}:success`
- `refund:{app_id}:{payment_id}:failed`
- `cancel:{app_id}:{sms_order_id}:auto_renew_disabled`
- `upgrade:{app_id}:{history_id}:payment_initiated`
- `upgrade:{app_id}:{history_id}:payment_success`
- `upgrade:{app_id}:{history_id}:payment_failed`
- `upgrade:{app_id}:{history_id}:old_subscription_cancelled`
- `upgrade:{app_id}:{history_id}:new_subscription_created`
- `webhook:{gateway}:{webhook_event_id}`
- `checkout:{app_id}:{sms_cust_id}:{checkout_session_id}:started`
- `gateway_order:{app_id}:{gateway_order_id}:payment_initiated`
- `renewal:{app_id}:{sms_subscription_id}:{billing_cycle}:payment_success`

### Rule D: field ownership

Use these sources in priority order:

- order identifiers:
  - `tbl_order.sms_order_id`
  - fallback request payload / resolved metadata
- subscription identifiers:
  - `tbl_customer_subscriptions.id`
  - `tbl_customer_subscriptions.client_subscription_id`
- gateway:
  - current gateway response object
  - resolved gateway from subscription/order metadata
- amounts:
  - gateway request/response for refund and payment
  - `tbl_order.total`
  - `tbl_customer_subscriptions.price`
- location:
  - current order country/state/city if present
  - fallback to latest known non-empty order country for manual journey

## Minimal Implementation Order

Implement in this order inside this repo:

1. `refundCustomerAjax`
   - `refund_initiated`
   - `refund_success`
   - `refund_failed`
   - `subscription_cancelled`

2. `cancelSubscription`
   - `subscription_cancelled` for manual/admin cancel
   - `auto_renew_disabled` for gateway recurring cancel

3. `SubscriptionUpgradeService`
   - `payment_initiated`
   - `payment_success`
   - `payment_failed`
   - `subscription_cancelled`
   - `subscription_created`
   - `auto_renew_enabled` / `auto_renew_disabled`

4. Go app API purchase backend
   - checkout/coupon/voucher/live payment events

5. Go payment webhook API
   - `webhook_received`

6. Go reconciliation worker
   - `reconciliation_started`
   - `reconciliation_completed`

7. Go renewal payment worker
   - `renewal_payment_*`
   - `subscription_renewed`

## Recommended Writer Shape

Every integration point should call one thin writer service with:

- `event_type`
- `event_status`
- `event_at`
- `app_id`
- `sms_cust_id`
- `sms_order_id`
- `sms_subscription_id`
- `gateway_name`
- `transaction_id`
- `currency`
- `final_amount`
- `reason`
- `remarks`
- `metadata`

Use existing `SubscriberJourneyClickhouseService` as the read/write home for normalized rows.

For Go services, implement the same normalized row contract in a small dedicated package, for example `journeywriter`, with a thin method such as:

```go
type JourneyWriter interface {
    InsertJourneyEvent(ctx context.Context, evt JourneyEvent) error
}
```

Go integration rules:

- keep the write append-only
- use deterministic idempotency keys per real business step
- do the ClickHouse insert after the current handler already knows the real result
- never let the ClickHouse write change the existing API response
- if Go owns gateway callbacks, emit `webhook_received` there rather than reconstructing it later in Laravel
- if Go owns coupon or voucher APIs, emit those events there rather than inferring them from order tables

## What Should Not Be Implemented In This Repo Right Now

Do not fake these sources inside admin controllers:

- customer app checkout start
- package selection from app
- coupon validation from app checkout
- voucher validation from app checkout
- live payment pending from gateway redirect flow
- gateway webhook receipt if callback endpoint is owned elsewhere
- reconciliation states if settlement worker is owned elsewhere

If those flows are not in this repo, document them and implement the writer in the service that truly owns them.

## Final Implementation Principle

The Journey timeline should become a **normalized audit stream of real events**, not a reconstructed guess from side tables.

For this repository, that means:

- use real local sources where they exist
- mark Go checkout/webhook/reconciliation/renewal sources as out-of-repo ownership from Laravel
- keep writes append-only and idempotent
- never change current business logic to satisfy the timeline
