mgpoller

package
v1.0.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 14, 2026 License: AGPL-3.0 Imports: 12 Imported by: 0

Documentation

Overview

Package mgpoller polls MoneyGram for SEP-24 transaction status changes and drives the existing disbursement state machine accordingly.

MoneyGram does not publish webhooks. The integration plan in internal-docs/moneygram-integration.md §10 makes polling the canonical driver for cash-pickup loans:

  1. After InitiateWithdrawal, MG returns an interactive URL. The user opens it (delivered via SMS by the USSD layer) and completes KYC.
  2. MG transitions the SEP-24 transaction to pending_user_transfer_start. The poller observes this and sends USDC from treasury to MG's anchor account using the memo embedded in the tx response.
  3. MG processes the payment and transitions to pending_user_transfer_complete. The poller backfills the locked payout amount, currency, and cash-pickup reference number on the loan row, then SMSes the reference to the user.
  4. completed / refunded / expired / error are terminal; the poller transitions disbursement_status accordingly and stops polling.

The poller mirrors pkg/webhook/refund_poller.go's lifecycle conventions: Start(ctx) runs a ticker loop and exits on context cancellation.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type AlertService

type AlertService interface {
	AlertOps(subject, message string) error
}

AlertService is the same interface used by the YC refund poller — receives ops alerts when something needs human attention. Optional.

type DisbursementUpdater

type DisbursementUpdater interface {
	UpdateDisbursementStatus(sequenceID string, status string) error
	NotifyDisbursementComplete(sequenceID string) error
	NotifyDisbursementFailed(sequenceID string) error

	// NotifyCashPickupReady tells the borrower their cash is collectable and
	// quotes the MG reference number. Sent once, when MG reports
	// pending_user_transfer_complete.
	NotifyCashPickupReady(sequenceID string) error

	// NotifyRefundReceived tells the borrower their cash pickup was cancelled
	// and the funds returned. Distinct from NotifyDisbursementFailed because
	// the usual cause is the borrower cancelling in MoneyGram's own UI,
	// sometimes by mistake — the message has to say they can request again.
	NotifyRefundReceived(sequenceID string) error

	RepayVault(sequenceID string) error

	// RepayVaultAmount repays an explicit stroop amount rather than the loan
	// principal. Used for refunds, where MG may return less than we sent and
	// repaying the full principal would overdraw the treasury.
	RepayVaultAmount(sequenceID string, amountStroops int64) error
}

DisbursementUpdater drives terminal state transitions and user notifications. The lending module's disbursement-status adapter already implements this for the YC flow; the same impl is reused here.

type LoanFetcher

type LoanFetcher interface {
	GetActiveMoneyGramLoans(ctx context.Context, limit int) ([]LoanRecord, error)
}

LoanFetcher retrieves active MoneyGram cash-pickup loans for the poller to evaluate. Implementations should select loans where ramp_provider = "moneygram" AND disbursement_status NOT IN terminal-states.

type LoanRecord

type LoanRecord struct {
	LoanID               string
	SequenceID           string  // = loans.ramp_sequence_id
	MoneyGramTxID        string  // = loans.ramp_request_id
	ChildAccountIndex    uint32  // for SEP-10 memo re-derivation
	PrincipalStroops     int64   // USDC stroops to send to MG anchor
	RequestedLocalAmount float64 // KES the user typed in USSD; used for drift alerts
	HasStellarSend       bool    // true once MG.tx.stellar_transaction_id is observed
	DisbursementStatus   string
	PhoneNumber          string
	UserID               string
}

LoanRecord is the projection of a loan row the poller needs to drive state for a single MoneyGram cash-pickup transaction.

The lending module's loan repository constructs these from active rows where ramp_provider="moneygram" and disbursement_status is in the active set.

type LoanRecorder

type LoanRecorder interface {
	// RecordTransactionUpdate persists the latest fields from a polled MG
	// transaction onto the loan row: amount_out, amount_out_asset, amount_fee,
	// external_transaction_id, more_info_url. Idempotent.
	RecordTransactionUpdate(ctx context.Context, loanID string, tx *stellaranchor.Transaction) error

	// RecordSendUSDC records the Stellar tx hash from a successful USDC
	// transfer to MG's anchor account, replacing the pending claim written by
	// RecordSendAttempt.
	RecordSendUSDC(ctx context.Context, loanID string, txHash string) error

	// RecordSendAttempt claims the send *before* the payment is submitted, so
	// a crash between submission and RecordSendUSDC cannot let the next tick
	// re-send. The claim makes LoanRecord.HasStellarSend true on reload.
	RecordSendAttempt(ctx context.Context, loanID string) error

	// ClearSendAttempt releases the claim. Only called when the payment
	// definitively did not move funds, so a later tick may safely retry.
	ClearSendAttempt(ctx context.Context, loanID string) error

	// RecordRefund persists the settled refund: the Stellar hash MG returned
	// the USDC in, the net stroops received, and any shortfall against the
	// principal we originally sent. Written before the vault repay so a crash
	// mid-repay leaves evidence of what came back.
	RecordRefund(ctx context.Context, loanID string, refund RefundRecord) error
}

LoanRecorder writes back the per-tick state changes the poller derives from MG's SEP-24 transaction object.

type PaymentVerifier

type PaymentVerifier interface {
	TransactionSucceeded(ctx context.Context, txHash string) (bool, error)

	// PaymentsTo returns the payments in txHash addressed to destination in
	// the named asset. Direction and amount both come from the ledger: an
	// anchor's outbound refund and our own inbound payment to that anchor are
	// both "successful transactions", so success alone cannot tell them apart.
	PaymentsTo(ctx context.Context, txHash, destination, assetCode, assetIssuer string) ([]rpc.Payment, error)
}

PaymentVerifier confirms that a Stellar transaction an anchor claims to have made actually succeeded on-ledger.

Optional: a nil verifier skips confirmation and trusts the anchor, which is logged. Supplying one means we never repay the vault against a refund that did not land.

type Poller

type Poller struct {
	// contains filtered or unexported fields
}

Poller drives the MoneyGram cash-pickup state machine.

func NewPoller

func NewPoller(
	client *moneygram.Client,
	fetcher LoanFetcher,
	recorder LoanRecorder,
	disbursement DisbursementUpdater,
	treasury offramp.TreasuryTransfer,
	verifier PaymentVerifier,
	alerts AlertService,
	cfg PollerConfig,
	logger *slog.Logger,
) (*Poller, error)

NewPoller validates the dependencies and returns a Poller. client, fetcher, recorder, disbursement, and treasury are required; verifier, alerts and logger may be nil.

func (*Poller) Start

func (p *Poller) Start(ctx context.Context)

Start runs the poller until ctx is cancelled. Mirrors RefundPoller.Start.

type PollerConfig

type PollerConfig struct {
	// PollInterval is how often the ticker fires. Default 30s.
	PollInterval time.Duration

	// MaxBatch caps the number of loans evaluated per tick. Default 100.
	MaxBatch int

	// PayoutDriftAlertPct triggers an ops alert when the locked
	// amount_out diverges from RequestedLocalAmount by more than this
	// fraction (e.g. 0.02 = 2 %). Default 0.02. Set to 0 to disable.
	PayoutDriftAlertPct float64

	// RefundSettleMaxAttempts caps how many ticks a loan may sit in
	// refund_pending waiting for MoneyGram to publish the SEP-24 refunds
	// object before ops are alerted. Observed MG behaviour is that it may
	// never arrive, so without a ceiling the loan polls silently forever.
	// Default 20 (10 minutes at the default interval).
	RefundSettleMaxAttempts int

	// RefundDestination is the account MoneyGram returns funds to — the
	// wallet we withdraw from. Defaults to the client's SEP-10 account.
	RefundDestination string

	// RefundAssetIssuer is the USDC issuer a refund payment must carry.
	// Defaults to the anchor client's configured issuer.
	RefundAssetIssuer string
}

PollerConfig configures cadence and drift detection.

func DefaultConfig

func DefaultConfig() PollerConfig

DefaultConfig returns a sensible PollerConfig.

type RefundRecord

type RefundRecord struct {
	// TxHash is the Stellar transaction the refund arrived in. When MG splits
	// a refund across several payments this is the last one; the amount is
	// always the total.
	TxHash string

	// NetStroops is what actually landed, summed from the refund payments as
	// they appear on-ledger rather than from the anchor's reported amounts.
	NetStroops int64

	// ShortfallStroops is PrincipalStroops - NetStroops when MG returned less
	// than we sent, otherwise zero. Non-zero means the treasury absorbed the
	// difference and the loan needs a human to settle it.
	ShortfallStroops int64
}

RefundRecord is the settled outcome of a MoneyGram refund.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL