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:
- After InitiateWithdrawal, MG returns an interactive URL. The user opens it (delivered via SMS by the USSD layer) and completes KYC.
- 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.
- 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.
- 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 ¶
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.
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.