PAYATHON 2026

MyFatoorah Payment Gateway 小数价格与 QTY 更新问题

支付老李

结论

KWD 支持三位小数,0.145、1.345 和 2.495 都是合法金额。如果价格变成整数,问题通常不在 KWD,而在金额传给 MyFatoorah 之前的处理过程,例如被转换成 Int、使用了错误的格式化方式,或先用二进制浮点数存储,之后又被取整。

QTY 没有更新,通常是因为第二次支付仍在使用第一次创建的支付请求、Invoice、Payment URL 或 SDK request 对象。取消支付后,需要根据当前购物车重新生成商品数组并创建新的支付请求,不能继续打开旧的支付页面。

价格为什么会丢失小数

先检查是否存在以下代码:

let price = Int("0.145")       // 错误
let price = Int(0.145)         // 错误,结果为 0
let price = String(format: "%.0f", 0.145) // 错误,强制保留 0 位小数

金额也不适合直接使用 Float:

let price: Float = 2.495

Float 和 Double 使用二进制浮点数,无法精确表示部分十进制金额。支付金额应优先使用 Decimal 或 NSDecimalNumber。

let price = Decimal(string: "2.495", locale: Locale(identifier: "en_US_POSIX"))

如果 SDK 的金额属性只接受 Double,可以在最后赋值给 SDK 时再转换:

guard let decimalPrice = Decimal(
    string: "2.495",
    locale: Locale(identifier: "en_US_POSIX")
) else {
    return
}

let sdkPrice = NSDecimalNumber(decimal: decimalPrice).doubleValue

这次转换只是为了适配 SDK 的类型。购物车计算和持久化仍应使用 Decimal。

正确构造商品数据

发送给 MyFatoorah 的商品明细应直接取自当前购物车,包括每件商品的 Quantity 和 UnitPrice。请求数据大致如下:

{
  "InvoiceValue": 5.860,
  "CurrencyIso": "KWD",
  "InvoiceItems": [
    {
      "ItemName": "Regular",
      "Quantity": 2,
      "UnitPrice": 0.145
    },
    {
      "ItemName": "Sugar Free",
      "Quantity": 4,
      "UnitPrice": 0.145
    },
    {
      "ItemName": "Corn Oil",
      "Quantity": 1,
      "UnitPrice": 2.495
    }
  ]
}

总金额应按以下公式计算:

0.145 × 2 + 0.145 × 4 + 2.495 × 1 = 3.365 KWD

因此,这个示例中的 InvoiceValue 应为 3.365,不是 5.860。实际请求的总金额必须与商品明细的计算结果一致。

具体字段名称要以项目当前使用的 MyFatoorah SDK 或 API 文档为准。不同版本的模型名称可能不同,但金额和数量的处理方式相同。

Swift 中可以这样计算总金额:

struct CartItem {
    let name: String
    let price: Decimal
    var quantity: Int
}

let items = [
    CartItem(name: "Regular", price: Decimal(string: "0.145")!, quantity: 2),
    CartItem(name: "Sugar Free", price: Decimal(string: "0.145")!, quantity: 4),
    CartItem(name: "Corn Oil", price: Decimal(string: "2.495")!, quantity: 1)
]

let total = items.reduce(Decimal.zero) { result, item in
    result + item.price * Decimal(item.quantity)
}

如果需要将金额固定为 KWD 的三位小数,可以在最终提交前进行十进制舍入:

func roundKWD(_ value: Decimal) -> Decimal {
    var source = value
    var result = Decimal()

    NSDecimalRound(
        &result,
        &source,
        3,
        .plain
    )

    return result
}

let invoiceValue = roundKWD(total)

不要把格式化后的本地化字符串作为金额传给 API,例如:

1,345
1.345 د.ك
KD 1.345

API 中的金额应是数值,不能带货币符号或千位分隔符。

QTY 更新后必须重新创建支付请求

支付页面通常显示创建 Invoice 或支付会话时提交的数据。购物车在 App 内发生变化后,已经创建的支付页面不会自动同步。

以下处理会再次打开旧的支付页面:

if paymentURL != nil {
    openPaymentPage(paymentURL!)
}

由于这里复用了旧 Invoice,页面仍会显示第一次提交的 QTY。

正确流程是:

用户修改购物车
    ↓
读取最新购物车数据
    ↓
重新构造 InvoiceItems
    ↓
重新计算 InvoiceValue
    ↓
创建新的支付请求或 Invoice
    ↓
使用新返回的支付信息打开 SDK screen

示意代码如下:

func startCheckout() {
    let currentItems = cartStore.items

    let invoiceItems = currentItems.map { item in
        [
            "ItemName": item.name,
            "Quantity": item.quantity,
            "UnitPrice": NSDecimalNumber(decimal: item.price)
        ] as [String: Any]
    }

    let total = currentItems.reduce(Decimal.zero) { result, item in
        result + item.price * Decimal(item.quantity)
    }

    let request: [String: Any] = [
        "CurrencyIso": "KWD",
        "InvoiceValue": NSDecimalNumber(decimal: roundKWD(total)),
        "InvoiceItems": invoiceItems
    ]

    createNewPaymentRequest(request)
}

createNewPaymentRequest 只是代指项目中调用 MyFatoorah API 或 SDK 的实际代码,并不是某个特定版本的官方方法名。

用户取消支付时,应清除仅属于本次支付会话的数据:

func paymentDidCancel() {
    currentPaymentURL = nil
    currentInvoiceId = nil
    currentPaymentRequest = nil
}

购物车不需要清除。用户再次点击支付时,再读取购物车的最新状态。

还需要检查对象复用问题

即使每次都重新打开 SDK screen,只要商品对象或请求对象仅创建过一次,提交的仍可能是旧数量。例如:

lazy var paymentRequest = buildPaymentRequest()

lazy 属性只初始化一次,不适合保存会随购物车变化的请求。应改为每次重新生成:

func makePaymentRequest(from items: [CartItem]) -> PaymentRequest {
    // 根据当前 items 创建一个全新的请求
}

使用可变数组时,还要检查是否只更新了界面显示的 QTY,却没有更新支付请求使用的数据源:

displayedItems[index].quantity = 2

如果支付时读取的是另一个旧数组,数量仍不会变化:

let checkoutItems = originalItems

购物车 UI、总金额和支付请求应读取同一份最新数据。

如何定位数据在哪一步被改错

应在打开 SDK screen 前记录最终请求,而不是只打印服务端最初返回的商品数据:

for item in currentItems {
    print(
        "name=\(item.name), " +
        "quantity=\(item.quantity), " +
        "price=\(NSDecimalNumber(decimal: item.price).stringValue)"
    )
}

print("invoiceValue=\(NSDecimalNumber(decimal: invoiceValue).stringValue)")

还要检查实际发送的 HTTP request body。如果请求中已经是:

{
  "Quantity": 4,
  "UnitPrice": 0.145
}

但支付页面仍显示旧值,应继续检查是否打开了旧的 Invoice ID 或 Payment URL。

如果实际请求中已经出现:

{
  "Quantity": 1,
  "UnitPrice": 0
}

说明问题发生在 App 的模型转换、缓存或序列化阶段,与支付页面无关。

注意事项

  • KWD 通常按三位小数处理,不要使用只保留两位小数的货币格式。
  • 不要先将金额乘以 100。只有接口明确要求传入最小货币单位时才能这样处理。KWD 使用三位小数,换算比例也不是普通两位小数货币使用的 100。
  • product_id 通常是商户自己的商品标识,不能替代 Quantity 和 UnitPrice。
  • InvoiceValue 应等于所有商品 UnitPrice × Quantity 的总和。
  • 取消支付不会修改已有 Invoice。商品数量变化后,需要创建新的支付会话。
  • MyFatoorah 不同 SDK 版本的类名和属性可能不同。应以项目当前安装版本的接口定义为准,不要直接照搬其他版本的示例类名。

备注:内容仅供参考。