如何在 Netsuite 中实现 Token based Payments?
结论
在 NetSuite 中实现 Token based Payments,并不是简单地把信用卡号替换成一段 token。能否实现,要看当前使用的 MeS(Merchant e-Solutions)支付集成是否支持以下功能:
- 在网关端对信用卡进行 tokenization;
- 将 token 关联到 NetSuite 客户或交易;
- 后续扣款时使用 token,不再提交 PAN、有效期和 CVV;
- 通过 NetSuite 的 Payment Instrument、SuitePayments 集成或网关 SuiteApp 处理授权、请款、退款和撤销。
如果现有 MeS connector 没有开放 tokenization 或 card-on-file 接口,仅配置 NetSuite 标准信用卡字段无法补齐这项能力。通常需要升级或更换支持 tokenization 的支付连接器,或者自行开发“外部卡库 + NetSuite 交易同步”集成。
推荐的实现结构
较稳妥的处理流程如下:
客户浏览器
│
│ 在 MeS 托管页面或 Hosted Fields 中输入卡号
▼
MeS Gateway / Vault
│
│ 返回 payment token、card brand、last4、expiry
▼
NetSuite
│
│ 只保存 token 和非敏感展示信息
▼
后续 Customer Payment / Sales Order / Cash Sale
│
│ 使用 token 向 MeS 发起授权或扣款
▼
保存 gateway transaction ID 和处理结果
信用卡数据应从客户浏览器直接进入 MeS。不要先将完整卡号发送到 Suitelet、RESTlet、自定义记录或脚本日志,再调用 tokenization API。这样做仍会扩大 PCI DSS 合规范围。
实施步骤
1. 确认 MeS 集成是否支持卡片令牌
先向当前 MeS connector 或 SuiteApp 的供应商确认:
- 是否支持 customer vault 或 card tokenization;
- token 是一次性的,还是可重复使用的 payment method token;
- 是否支持 merchant-initiated transaction;
- 能否使用 token 执行 authorization、capture、sale、refund 和 void;
- NetSuite 中应该启用哪个 Payment Processing Profile;
- token 是否会自动保存为 Payment Instrument;
- 是否提供 Hosted Payment Page、Hosted Fields 或类似的浏览器端采集组件。
一次性 token 通常只能用于当前交易,不能用于后续收款。周期性扣款或保存卡片需要使用可重复的 vault token。
2. 优先使用 NetSuite 原生 Payment Instrument
如果已安装的支付 SuiteApp 支持 NetSuite Payment Instruments,建议优先采用这种方式。典型操作包括:
- 在 NetSuite 中启用支付工具及相关支付功能;
- 安装或升级 MeS 提供的兼容 SuiteApp;
- 创建 Payment Processing Profile;
- 配置商户账户、支持的币种和支付方式;
- 通过网关提供的托管页面录入信用卡;
- 由 connector 将返回的 token 创建为客户的 Payment Instrument;
- 在 Sales Order、Cash Sale 或 Customer Payment 中选择该 Payment Instrument。
使用这种方式时,token 的传递以及授权、请款和退款通常都由连接器处理,脚本不需要直接操作信用卡字段。
不同 NetSuite 账户启用的功能和安装的支付 SuiteApp 可能不同,对外提供的记录及字段也会有所差异,因此不能假定某个字段 ID 在所有账户中都存在。
3. 如果连接器不支持,则使用外部 Vault
如果 MeS 提供 tokenization API,但现有 NetSuite connector 不支持,可以开发自定义集成:
- 前端使用 MeS 的 Hosted Fields 或托管支付页面;
- MeS 返回可重复使用的 token;
- NetSuite 保存 token、网关客户 ID、卡品牌、末四位和有效期;
- SuiteScript 调用服务器端集成服务;
- 集成服务使用 token 向 MeS 发起交易;
- NetSuite 根据返回结果创建或更新 Customer Payment;
- 保存网关交易 ID,供 capture、refund、void 和对账使用。
token 映射建议保存在权限受到严格控制的自定义记录中,不要写入可随意搜索、导出或显示的普通文本字段。token 通常不是完整信用卡号,但仍应作为敏感支付凭证管理。
SuiteScript 示例
下面的代码只用于说明集成结构。customrecord_gateway_token、自定义字段和 MeS 接口路径都是示例,必须替换为实际 NetSuite 账户配置和 MeS API 定义。
/**
* @NApiVersion 2.1
* @NScriptType Restlet
*/
define(['N/https', 'N/record', 'N/runtime', 'N/search', 'N/error'],
(https, record, runtime, search, error) => {
function post(request) {
const {
customerId,
invoiceId,
amount,
currency,
paymentTokenId
} = request;
const tokenData = loadPaymentToken(paymentTokenId, customerId);
const response = https.post({
url: runtime.getCurrentScript().getParameter({
name: 'custscript_mes_charge_url'
}),
headers: {
'Content-Type': 'application/json',
'Authorization': buildAuthorizationHeader()
},
body: JSON.stringify({
amount,
currency,
paymentToken: tokenData.gatewayToken,
merchantReference: `INV-${invoiceId}`
})
});
const result = JSON.parse(response.body || '{}');
if (response.code < 200 || response.code >= 300 || !result.approved) {
throw error.create({
name: 'PAYMENT_DECLINED',
message: result.message || `Gateway returned HTTP ${response.code}`
});
}
const payment = record.transform({
fromType: record.Type.INVOICE,
fromId: invoiceId,
toType: record.Type.CUSTOMER_PAYMENT,
isDynamic: true
});
payment.setValue({
fieldId: 'payment',
value: amount
});
// 示例字段:实际字段必须以当前 NetSuite 账户为准。
payment.setValue({
fieldId: 'custbody_gateway_transaction_id',
value: result.transactionId
});
payment.setValue({
fieldId: 'custbody_gateway_payment_token_ref',
value: paymentTokenId
});
const paymentId = payment.save({
enableSourcing: true,
ignoreMandatoryFields: false
});
return {
success: true,
paymentId,
gatewayTransactionId: result.transactionId
};
}
function loadPaymentToken(paymentTokenId, customerId) {
const tokenRecord = search.lookupFields({
type: 'customrecord_gateway_token',
id: paymentTokenId,
columns: [
'custrecord_token_customer',
'custrecord_gateway_token_value',
'isinactive'
]
});
const ownerId = tokenRecord.custrecord_token_customer?.[0]?.value;
if (String(ownerId) !== String(customerId) || tokenRecord.isinactive) {
throw error.create({
name: 'INVALID_PAYMENT_TOKEN',
message: 'Payment token does not belong to this customer or is inactive.'
});
}
return {
gatewayToken: tokenRecord.custrecord_gateway_token_value
};
}
function buildAuthorizationHeader() {
// 凭证应存放在 NetSuite API Secrets 或其他安全凭证存储中。
// 不要将 API key、密码或完整 Authorization 值写入脚本。
return 'Bearer ' + runtime.getCurrentScript().getParameter({
name: 'custscript_mes_access_token'
});
}
return { post };
});
这段示例还有一个重要前提:网关已经完成扣款。如果随后保存 Customer Payment 失败,应将该交易放入待同步队列。不能直接再次向网关发起扣款,否则可能产生重复交易。
前端 tokenization 示例
浏览器端应使用 MeS 官方提供的 JavaScript SDK 或托管组件。下面的代码只是抽象示意,不是 MeS 的真实 API:
const result = await mesHostedFields.tokenize({
cardholderName: document.querySelector('#cardholder-name').value,
billingPostalCode: document.querySelector('#postal-code').value
});
await fetch('/app/site/hosting/restlet.nl?script=123&deploy=1', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
customerId: 456,
invoiceId: 789,
amount: '125.00',
currency: 'USD',
gatewayToken: result.token
})
});
不要根据这段示例推测 MeS 的 SDK 方法名。实际开发必须以 MeS 提供的 Hosted Payment、tokenization 或 vault API 文档为准。
必须处理的交易细节
幂等与重复扣款
每次请求都应生成唯一的 idempotency key 或 merchant reference,并保存以下信息:
- NetSuite transaction ID;
- MeS transaction ID;
- 请求状态;
- 授权金额;
- capture 状态;
- 最近一次错误信息。
发生网络超时时,不能直接将交易判定为扣款失败。应先根据 merchant reference 查询网关状态,再决定是否重试。
授权与请款
实现前需要明确使用哪种交易模式:
sale:完成授权后立即请款;authorization:只冻结额度;capture:订单履约后对原授权请款;void:在授权尚未结算时撤销;refund:结算完成后退款。
Sales Order 通常需要先执行 authorization,再进行 capture;Customer Payment 则更接近已经完成的收款记录。具体采用哪种交易模型,仍取决于现有业务流程以及 MeS connector 的处理方式。
CVV
不要保存 CVV。CVV 通常也不能在 token 化后用于后续周期性扣款。首次录卡时,可以将 CVV 直接提交给网关验证;后续 token payment 应按照 card-on-file 或 merchant-initiated transaction 的规则处理。
权限和日志
不要在以下位置记录完整卡号、CVV、支付请求原文或认证信息:
- SuiteScript execution log;
- 自定义记录;
- System Notes;
- 邮件;
- 集成错误日志;
- 浏览器控制台。
API 凭证应存放在安全凭证存储中,同时限制脚本部署、角色、自定义记录和搜索结果的访问权限。
最实际的落地建议
先确认当前 MeS SuiteApp 或 connector 是否支持 NetSuite Payment Instruments 和 gateway vault。如果支持,按照供应商提供的 Payment Processing Profile 流程配置即可,无需自行保存或传递 token。
如果不支持,应先向 MeS 获取正式的 tokenization、vault 和后续扣款 API 文档,再开发外部 Vault 集成。如果 MeS 无法提供与当前 NetSuite 支付流程兼容的 token 接口,更换已经支持 NetSuite tokenization 的支付提供商,通常比改写标准信用卡处理流程更可靠。
备注:内容仅供参考。
