Skip to content

feat: add provider fee tracking to payment orders#733

Open
Dprof-in-tech wants to merge 1 commit intomainfrom
feat-track-provider-fees-local-currency-orders
Open

feat: add provider fee tracking to payment orders#733
Dprof-in-tech wants to merge 1 commit intomainfrom
feat-track-provider-fees-local-currency-orders

Conversation

@Dprof-in-tech
Copy link
Collaborator

@Dprof-in-tech Dprof-in-tech commented Mar 19, 2026

Description

This pull request introduces support for tracking and aggregating provider fees in payment orders. The main changes add a new provider_fee field to the database and update relevant application logic to handle this new field, including mutation handling, statistics calculation, and schema management.

Provider Fee Field Addition and Schema Migration:

  • Added a new provider_fee column to the payment_orders table via a migration (20260319165304_add_provider_fee.sql) and updated migration checksums in atlas.sum and schema definitions in schema.go. [1] [2] [3] [4]

Application Logic Updates:

  • Updated the PaymentOrder struct and related methods to include the new provider_fee field, ensuring correct assignment, scanning, and string representation. [1] [2] [3] [4]
  • Extended the PaymentOrderMutation struct and its methods to support setting, adding, resetting, and querying the provider_fee field, mirroring existing fee fields. [1] [2] [3] [4] [5] [6] [7] [8] [9] [10] [11]

Statistics Calculation Enhancements:

  • Updated the Stats method in SenderController to aggregate and convert provider fees alongside sender fees and total volume, and include provider fee totals in the statistics response. [1] [2] [3] [4] [5]

Schema and Field Constants Update:

  • Added provider_fee to payment order field constants, columns, and default value declarations to ensure consistency across the codebase. [1] [2] [3]

Schema Index and Foreign Key Adjustments:

  • Updated column indices and foreign key references in schema.go to accommodate the new provider_fee field, ensuring migration and schema integrity. [1] [2]

References

closes #597

Testing

  • This change adds test coverage for new/changed/fixed functionality

Checklist

  • I have added documentation and tests for new/changed functionality in this PR
  • All active GitHub checks for tests, formatting, and security are passing
  • The correct base branch is being used, if not main

By submitting a PR, I agree to Paycrest's Contributor Code of Conduct and Contribution Guide.

Summary by CodeRabbit

Release Notes

  • New Features
    • Provider fees are now tracked at the payment order level, offering enhanced transparency into fee components
    • Payment statistics have been extended to report total provider fees alongside existing earnings metrics
    • The system now processes fee split events from blockchain transactions, automatically recording provider amounts and updating relevant payment records

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 19, 2026

📝 Walkthrough

Walkthrough

This PR adds provider fee tracking to payment orders by introducing a new provider_fee column to the database, extending Ent entity models to support the field, implementing event decoding for LocalTransferFeeSplitEvent from blockchain transactions, and wiring fee updates into the order processing pipeline.

Changes

Cohort / File(s) Summary
Database Schema & Migration
ent/migrate/migrations/20260319165304_add_provider_fee.sql, ent/migrate/schema.go
New provider_fee column added to payment_orders table as numeric NOT NULL DEFAULT 0. Schema indices updated to account for column insertion and field ordering adjustments.
Ent Entity ORM Generation
ent/schema/paymentorder.go, ent/paymentorder.go, ent/mutation.go, ent/paymentorder/paymentorder.go, ent/paymentorder/where.go, ent/paymentorder_create.go, ent/paymentorder_update.go, ent/runtime/runtime.go
Full Ent support for provider_fee field: schema definition, entity struct, mutation/CRUD builders (Set/Add/Update methods), query predicates (EQ/NEQ/GT/LT comparisons), default initialization, and runtime wiring.
Event Types & Decoding
utils/rpc_events.go, types/types.go
New LocalTransferFeeSplitEvent struct with block/tx identifiers and fee amounts. Added event signature constant and decoder function to extract orderId, senderAmount, providerAmount, and aggregatorAmount from blockchain logs.
Event Processing & Service Logic
services/indexer/evm.go, services/common/indexer.go, services/common/order.go
Indexer now decodes LocalTransferFeeSplit events during transaction processing. New UpdateProviderFee function queries and updates payment orders by gateway ID and network. Event processor routes decoded events to update service.
Controller Response Update
controllers/sender/sender.go
Stats endpoint aggregates provider fees from USD orders and converts local stablecoin provider fees via fiat rates, then includes TotalProviderFees in response alongside existing fee metrics.

Sequence Diagram(s)

sequenceDiagram
    participant Blockchain as Blockchain Network
    participant Indexer as EVM Indexer
    participant Decoder as Event Decoder
    participant Service as Fee Service
    participant Database as Payment Orders DB

    Blockchain->>Indexer: Emit LocalTransferFeeSplit Event (orderId, providerAmount, ...)
    Indexer->>Decoder: Decode RPC Log via DecodeLocalTransferFeeSplitEvent
    Decoder-->>Indexer: Extract orderId, amounts, return map
    Indexer->>Service: ProcessLocalTransferFeeSplitEvents(network, events)
    Service->>Service: For each event: UpdateProviderFee(network, event)
    Service->>Database: Query PaymentOrder by GatewayID & Network
    Service->>Database: Set provider_fee = event.ProviderAmount
    Database-->>Service: Update result (error or success)
    Service-->>Indexer: Log error if update fails, continue
    Indexer->>Database: Store LocalTransferFeeSplit event count
Loading

Estimated Code Review Effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly Related PRs

Suggested Reviewers

  • 5ran6
  • onahprosper
  • chibie

Poem

🐰 A fee so fine, now split with care,
Provider's portion, tracked with flair!
From blocks we glean, the numbers true,
Orders stored with fees anew. ✨💰

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Linked Issues check ❓ Inconclusive The PR implements core data model requirements from #597: adding provider_fee field to payment_orders table, updating PaymentOrder struct/mutations, and aggregating provider fees in statistics. However, the PR does not address all acceptance criteria from the issue. Verify that the provider fee calculation/assignment logic from LocalTransferFeeSplitEvent is complete, and clarify whether renaming sender_fee to sender_fee_amount was intended per AC1.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: add provider fee tracking to payment orders' clearly and accurately summarizes the primary change: adding a provider_fee field and related logic to track provider fees on payment orders.
Description check ✅ Passed The PR description comprehensively covers the changes with detailed sections explaining schema migration, application logic updates, statistics enhancements, and field/index adjustments. The Testing section is empty (unchecked) and documentation/tests are marked as TODO.
Out of Scope Changes check ✅ Passed All changes align with the issue scope: provider_fee field addition, mutation/update logic, statistics aggregation, and event processing for fee assignments. No unrelated changes detected.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat-track-provider-fees-local-currency-orders
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Tip

CodeRabbit can scan for known vulnerabilities in your dependencies using OSV Scanner.

OSV Scanner will automatically detect and report security vulnerabilities in your project's dependencies. No additional configuration is required.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (3)
controllers/sender/sender.go (1)

2937-2969: Don’t derive historical provider-fee totals from today’s FX rates.

This path divides paymentOrder.ProviderFee by the current MarketBuyRate/MarketSellRate, so TotalProviderFees will drift as rates move. If this metric is meant for reconciliation/reporting, prefer an order-time USD amount or a rate captured at settlement.

Also applies to: 2984-2988

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@controllers/sender/sender.go` around lines 2937 - 2969, The code is
incorrectly converting historical paymentOrder.ProviderFee using current
MarketBuyRate/MarketSellRate (via fiatCurrency.MarketBuyRate/MarketSellRate),
causing TotalProviderFees to drift; change the logic to use the order-recorded
USD amount or the FX rate captured at order/settlement time (e.g.,
paymentOrder.ProviderFeeUSD or paymentOrder.SettlementRate or
paymentOrder.FxRate) when computing localStablecoinProviderFee instead of
dividing by fiatCurrency.MarketBuyRate/MarketSellRate; if such a stored field is
missing, fall back to skipping conversion and log a warning so historical totals
remain stable (update references in the loop handling paymentOrder.ProviderFee
and the similar block around the later lines 2984-2988).
utils/rpc_events.go (2)

235-237: Tighten fixed-size ABI payload validation to exact length.

For this event ABI, payload should be exactly 96 bytes (3 × uint256). Using len(log.Data) < 96 is permissive and can mask malformed payloads.

Proposed patch
-	if len(log.Data) < 96 {
-		return nil, fmt.Errorf("invalid LocalTransferFeeSplit event data: too short")
+	if len(log.Data) != 96 {
+		return nil, fmt.Errorf("invalid LocalTransferFeeSplit event data: expected 96 bytes, got %d", len(log.Data))
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@utils/rpc_events.go` around lines 235 - 237, The length check for the
LocalTransferFeeSplit event is too permissive; replace the current conditional
that checks len(log.Data) < 96 with a strict equality check (len(log.Data) !=
96) and update the error message to state "invalid LocalTransferFeeSplit event
data: expected 96 bytes" so malformed payloads of incorrect length are rejected;
locate the validation around the LocalTransferFeeSplit handling where log.Data
is inspected and adjust the condition and message accordingly.

22-25: Add a verification test for event signature hashes to catch ABI drift.

The hardcoded event signature hashes are currently correct, but there's no test to verify they remain consistent with their canonical signatures. If the contract ABI changes, these constants could silently drift and break indexing. Add a test that computes keccak256 of the event signatures and asserts they match the constants.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@utils/rpc_events.go` around lines 22 - 25, Add a unit test that computes the
keccak256 hash of the canonical event signature strings and asserts they match
the existing constants SettleInEventSignature and
LocalTransferFeeSplitEventSignature in utils/rpc_events.go; specifically, derive
the hash from the exact signature text (e.g. "SettleIn(bytes32...)" and
"LocalTransferFeeSplit(bytes32 indexed orderId, uint256 senderAmount, uint256
providerAmount, uint256 aggregatorAmount)"), compare to the constants and fail
the test if they differ so ABI drift is detected.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@ent/schema/paymentorder.go`:
- Around line 45-47: The schema currently uses
field.Float("provider_fee").GoType(decimal.Decimal{}).DefaultFunc(...), which
sets a zero value and conflates "unknown" with genuine zero; remove the
DefaultFunc and make the column nullable/optional instead (e.g., use
field.Float("provider_fee").GoType((*decimal.Decimal)(nil)).Optional().Nillable()
or the project-preferred Optional/Nillable pattern) so new rows and migrated
rows can represent "fee not yet set" as NULL; update any code that reads/writes
provider_fee to handle a nil/*decimal.Decimal rather than assuming a non-nil
decimal.

In `@services/common/order.go`:
- Around line 61-72: The current code uses db.Client.PaymentOrder.Update() with
paymentorder.GatewayIDEQ(...) and related predicates which performs a bulk
update and ignores the affected-row count; instead, first fetch a single
PaymentOrder using
db.Client.PaymentOrder.Query().Where(paymentorder.GatewayIDEQ(gatewayID),
paymentorder.HasTokenWith(tokenent.HasNetworkWith(networkent.IdentifierEQ(network.Identifier)))).Only(ctx)
to resolve exactly one order (handle not-found and not-singular errors), then
call
db.Client.PaymentOrder.UpdateOneID(order.ID).SetProviderFee(event.ProviderAmount).Save(ctx)
and check/return/save any errors from both the query and the update (do not
treat “no matching order” as success).

In `@services/indexer/evm.go`:
- Around line 854-892: The code currently takes the raw uint256 string parsed
into providerAmount and stores it into LocalTransferFeeSplitEvent.ProviderAmount
(which later flows into UpdateProviderFee) without converting from base/minor
units to token decimals; change the creation of providerAmount so it is
normalized to human/token decimals before assigning to feeSplitEvent (use the
existing token-decimals lookup or a helper like
utils.NormalizeTokenAmount/WeiToDecimal with the appropriate token decimals),
i.e., parse providerAmountStr as you do now, then scale it by 10^-decimals (or
call the project normalization helper) and use that normalized decimal for
LocalTransferFeeSplitEvent.ProviderAmount so UpdateProviderFee receives the
correct decimal amount.

---

Nitpick comments:
In `@controllers/sender/sender.go`:
- Around line 2937-2969: The code is incorrectly converting historical
paymentOrder.ProviderFee using current MarketBuyRate/MarketSellRate (via
fiatCurrency.MarketBuyRate/MarketSellRate), causing TotalProviderFees to drift;
change the logic to use the order-recorded USD amount or the FX rate captured at
order/settlement time (e.g., paymentOrder.ProviderFeeUSD or
paymentOrder.SettlementRate or paymentOrder.FxRate) when computing
localStablecoinProviderFee instead of dividing by
fiatCurrency.MarketBuyRate/MarketSellRate; if such a stored field is missing,
fall back to skipping conversion and log a warning so historical totals remain
stable (update references in the loop handling paymentOrder.ProviderFee and the
similar block around the later lines 2984-2988).

In `@utils/rpc_events.go`:
- Around line 235-237: The length check for the LocalTransferFeeSplit event is
too permissive; replace the current conditional that checks len(log.Data) < 96
with a strict equality check (len(log.Data) != 96) and update the error message
to state "invalid LocalTransferFeeSplit event data: expected 96 bytes" so
malformed payloads of incorrect length are rejected; locate the validation
around the LocalTransferFeeSplit handling where log.Data is inspected and adjust
the condition and message accordingly.
- Around line 22-25: Add a unit test that computes the keccak256 hash of the
canonical event signature strings and asserts they match the existing constants
SettleInEventSignature and LocalTransferFeeSplitEventSignature in
utils/rpc_events.go; specifically, derive the hash from the exact signature text
(e.g. "SettleIn(bytes32...)" and "LocalTransferFeeSplit(bytes32 indexed orderId,
uint256 senderAmount, uint256 providerAmount, uint256 aggregatorAmount)"),
compare to the constants and fail the test if they differ so ABI drift is
detected.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f5322c92-006e-4763-9407-41e972240248

📥 Commits

Reviewing files that changed from the base of the PR and between 418b108 and ecf73d3.

⛔ Files ignored due to path filters (1)
  • ent/migrate/migrations/atlas.sum is excluded by !**/*.sum
📒 Files selected for processing (16)
  • controllers/sender/sender.go
  • ent/migrate/migrations/20260319165304_add_provider_fee.sql
  • ent/migrate/schema.go
  • ent/mutation.go
  • ent/paymentorder.go
  • ent/paymentorder/paymentorder.go
  • ent/paymentorder/where.go
  • ent/paymentorder_create.go
  • ent/paymentorder_update.go
  • ent/runtime/runtime.go
  • ent/schema/paymentorder.go
  • services/common/indexer.go
  • services/common/order.go
  • services/indexer/evm.go
  • types/types.go
  • utils/rpc_events.go

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Tracking Provider Fees for Local Currency Orders

1 participant