Files
commerce/thirdparty/paypal/ipn/ipnmessage.go
zeekay 1943f1c093 Hanzo Commerce
Multi-tenant commerce and billing: orders, subscriptions, invoices, plans,
usage metering and a credit ledger. One Go binary. MIT OR Apache-2.0.

Claude-Session: https://claude.ai/code/session_01SAtaDFm8VDiWrhYHNNtxMw
2026-09-23 21:52:16 -07:00

54 lines
1.9 KiB
Go

// Reference: https://developer.paypal.com/webapps/developer/docs/classic/ipn/integration-guide/IPNIntro/
package ipn
import (
"fmt"
"net/url"
"strings"
"github.com/hanzoai/commerce/models/types/currency"
)
type IpnMessage struct {
Status string // transaction[0].status
PayerEmail string // sender_email
PayeeEmail string // transaction[0].receiver
Amount currency.Cents // extracted from transaction[0].amount
PayKey string // pay_key
Currency currency.Type
}
// NewIpnMessage parses a PayPal IPN form.
//
// It returns an error rather than a zero-amount message: an IPN whose amount
// does not parse is a payment notification we cannot attribute, and recording
// it as 0 would post a free transaction that reports success. The amount field
// is "<CURRENCY> <decimal>", so a field with no space is malformed too — it
// used to index parts[1] unguarded and panic.
func NewIpnMessage(form url.Values) (*IpnMessage, error) {
message := new(IpnMessage)
message.Status = form.Get("transaction[0].status")
message.PayerEmail = form.Get("sender_email")
message.PayeeEmail = form.Get("transaction[0].receiver")
message.PayKey = form.Get("pay_key")
raw := form.Get("transaction[0].amount")
parts := strings.Fields(raw)
if len(parts) != 2 {
return nil, fmt.Errorf("ipn: amount %q is not \"<CURRENCY> <decimal>\"", raw)
}
// The currency is read first because it decides the scale — it is in the
// same field as the digits, so there is nothing to guess. Parsing at a
// hardcoded USD scale read a zero-decimal notification a hundred times too
// large: "JPY 500" is 500 minor units, not 50000.
message.Currency = currency.Type(strings.ToLower(parts[0]))
amount, err := message.Currency.Parse(parts[1])
if err != nil {
return nil, fmt.Errorf("ipn: %w", err)
}
message.Amount = amount
return message, nil
}