Back to Midday

Credit Card Transaction Handling

docs/credit-card-transaction-handling.md

latest8.1 KB
Original Source

Credit Card Transaction Handling

This document explains how Midday handles credit card transactions from different banking providers to ensure correct categorization and amount signs.

The Problem

Credit card transactions have unique semantics compared to regular bank accounts:

  • Purchase: Money goes OUT (you're spending)
  • Payment to card: Money goes IN (you're paying off balance)
  • Refund: Money goes IN (merchant refunded you)
  • Cashback/Rewards: Money goes IN (card benefit)

The challenge is that different providers report these transactions with different sign conventions, and money coming IN to a credit card is NOT income - it could be a payment, refund, or reward.

Provider Sign Conventions

Teller (US)

Account TypeTransactionTeller ReportsAfter Transform
CreditPurchase $100+100-100 (expense)
CreditPayment $200-200+200 (payment)
DepositoryDeposit $500+500+500 (income)
DepositoryWithdrawal $50-50-50 (expense)

Transform: Credit accounts have sign inverted (amount * -1), depository accounts use raw value.

Plaid (US)

TransactionPlaid ReportsAfter Transform
Money OUT+amount-amount
Money IN-amount+amount

Transform: ALL amounts are inverted (amount * -1). Plaid convention is "positive = money out".

Enable Banking (EU - PSD2)

TransactionAPI ReportsResult
Money INcredit_debit_indicator: "CRDT" + positive amountPositive
Money OUTcredit_debit_indicator: "DBIT" + positive amountNegative

Transform: Uses credit_debit_indicator field. No account-type-specific handling needed.

GoCardless (EU - PSD2)

TransactionAPI ReportsResult
Money INPositive amountPositive
Money OUTNegative amountNegative

Transform: Uses signed amounts directly. No transformation needed.

Category Logic

After amount transformation, all providers use consistent category logic:

if (amount > 0) {
  if (accountType === "credit") {
    // Money IN to credit card - NOT automatically income
    if (looksLikePayment(transaction)) {
      return "credit-card-payment";
    }
    // Might be refund, cashback, etc. - let user categorize
    return null;
  }
  return "income";  // Depository account
}
return null;  // Negative amounts (expenses) - let user categorize

How "looksLikePayment" Works Per Provider

ProviderPayment Indicators
Tellertransaction.type is payment, bill_payment, digital_payment, ach, or transfer
Plaidpersonal_finance_category.primary is LOAN_PAYMENTS or TRANSFER_IN, or transaction_code is "bill payment"
Enable Bankingbank_transaction_code.description is "Transfer" or "Payment"
GoCardlessproprietaryBankTransactionCode is "Transfer" or "Payment"

Special Cases

  1. Cashback/Rewards (Teller): If transaction.details.category === "income", categorized as "income"
  2. Plaid INCOME category: If personal_finance_category.primary === "INCOME", categorized as "income"
  3. Refunds: Return null so user can manually categorize (could be miscategorized as income otherwise)

Account Type Mapping

Enable Banking & GoCardless (ISO 20022 Cash Account Types)

cashAccountType / cash_account_typeMidday Type
CARDcredit
LOANother_asset
CACC (Current Account)depository
SVGS (Savings)depository
TRAN (Transaction)depository
CASHdepository
Othersdepository

Teller & Plaid

Account type is explicitly provided by the API (e.g., credit, depository).

Test Coverage

Each provider has tests covering:

  1. ✅ Depository income transaction → "income"
  2. ✅ Depository expense transaction → null
  3. ✅ Credit card purchase → null (negative amount)
  4. ✅ Credit card payment → "credit-card-payment"
  5. ✅ Credit card refund → null (user categorizes)
  6. ✅ Account type mapping (for EU providers)

Why This Matters

Incorrect handling could cause:

  • Credit card payments appearing as income in reports
  • Inflated income figures (every card payment counted as income)
  • Incorrect profit/loss calculations
  • Tax reporting issues

The fix ensures:

  • Credit card payments are correctly categorized as "credit-card-payment" (a transfer, not income)
  • Refunds are not auto-categorized (user decides)
  • Only actual income on depository accounts is marked as "income"

Files Modified

  • apps/engine/src/providers/teller/transform.ts
  • apps/engine/src/providers/plaid/transform.ts
  • apps/engine/src/providers/enablebanking/transform.ts
  • apps/engine/src/providers/gocardless/transform.ts
  • Corresponding test files and snapshots

Verification Sources

Plaid Amount Convention (VERIFIED)

From Plaid's official API documentation and confirmed in our codebase comment:

"Positive values when money moves out of the account; negative values when money moves in. For example, debit card purchases are positive; credit card payments, direct deposits, and refunds are negative."

Source: Plaid Transactions API - amount field

Our implementation inverts ALL Plaid amounts, which converts their convention to: positive = money IN, negative = money OUT.

Teller Amount Convention (VERIFIED)

From Teller's API documentation:

"The signed amount of the transaction as a string."

For credit accounts specifically, Teller uses opposite signs:

  • Positive = money OUT (purchase)
  • Negative = money IN (payment)

Source: Teller Transactions API

Our implementation inverts ONLY credit account amounts.

Enable Banking / GoCardless (PSD2 Standard - VERIFIED)

Both providers follow the Berlin Group NextGenPSD2 standard which uses:

  • credit_debit_indicator: "CRDT" (Credit = money IN) or "DBIT" (Debit = money OUT)
  • cashAccountType: ISO 20022 account type codes (CACC, CARD, SVGS, etc.)

Sources:

ISO 20022 Cash Account Types (VERIFIED)

CodeNameDescription
CACCCurrentAccount for transactional operations
CARDCardAccountAccount for card payments
CASHCashPaymentCash payment account
SVGSSavingsSavings account
TRANTransactionTransaction account
LOANLoanLoan account

Source: ISO 20022 External Cash Account Type Code

Implementation Confidence Level

ProviderSign ConventionCategory LogicConfidence
Plaid✅ Documented in code, matches Plaid docs✅ Uses Plaid's own categoriesHIGH
Teller✅ Documented in code, verified with API✅ Uses transaction type fieldHIGH
Enable Banking✅ Uses standard CRDT/DBIT indicator✅ Uses bank_transaction_codeHIGH
GoCardless✅ Uses standard signed amounts✅ Uses proprietaryBankTransactionCodeHIGH

References