Xero Contacts API 为何只更新 Bills 而忽略 Sales?
结论
这份 PaymentTerms payload 结构没有问题,也不需要增加 OAuth scope、请求头或 SalesDefaultAccountCode。
首先要确认目标联系人在 Xero 中是否已经是客户,也就是返回数据中是否包含:
"IsCustomer": true
Xero 会根据联系人实际参与的业务区分客户和供应商:
Bills是供应商的采购付款条件;Sales是客户的销售付款条件。
如果联系人只有 IsSupplier: true,而 IsCustomer: false,Xero 可能仍会接受整个请求并返回 HTTP 200,但只应用 Bills,忽略与当前联系人身份不符的 Sales。HTTP 200 只表示 Xero 已处理请求,不代表所有嵌套字段都已更新。
先确认联系人身份
更新前,先查询目标联系人:
GET /api.xro/2.0/Contacts/{ContactID}
xero-tenant-id: <tenant-id>
Authorization: Bearer <token>
Accept: application/json
检查响应中的以下字段:
{
"Contacts": [
{
"ContactID": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"IsCustomer": false,
"IsSupplier": true,
"PaymentTerms": {
"Bills": {
"Day": 7,
"Type": "DAYSAFTERBILLDATE"
},
"Sales": {
"Day": 30,
"Type": "DAYSAFTERBILLDATE"
}
}
}
]
}
如果 IsCustomer 是 false,通常可以据此解释为什么 Sales 没有更新。
不能通过 payload 强制设置客户身份
不要在联系人更新请求中加入:
"IsCustomer": true,
"IsSupplier": true
这两个字段用于反映联系人是否参与过应收或应付业务,通常由 Xero 根据交易记录维护,并不是可靠的可写开关。即使传入这些字段,Xero 也可能忽略它们。
联系人需要通过真实业务成为 Xero 中的客户,例如已有以该联系人为对象的销售交易。不要为了改变身份标志而创建虚假发票,应按照实际业务流程建立应收关系。
当联系人同时具有以下状态时:
"IsCustomer": true,
"IsSupplier": true
原来的请求结构就可以同时更新两组付款条件:
{
"Contacts": [
{
"ContactID": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"PaymentTerms": {
"Bills": {
"Day": 7,
"Type": "DAYSAFTERBILLDATE"
},
"Sales": {
"Day": 7,
"Type": "DAYSAFTERBILLDATE"
}
}
}
]
}
Apex 调用可以继续使用现有结构
现有调用结构可以保留,但应补充参数检查,并在调用后核对响应中的联系人数据,不能只记录 HTTP 状态码:
@future(callout=true)
public static void updatePaymentTerms(
String contactId,
String term,
String tenantId,
String namedCredentialName
) {
if (
String.isBlank(contactId) ||
String.isBlank(tenantId) ||
String.isBlank(namedCredentialName)
) {
return;
}
XeroPaymentTermMapper.XeroPaymentTerm terms =
XeroPaymentTermMapper.mapPaymentTerm(term);
if (terms == null) {
return;
}
Map<String, Object> paymentTerm = new Map<String, Object>{
'Day' => terms.Day,
'Type' => terms.Type
};
Map<String, Object> payload = new Map<String, Object>{
'Contacts' => new List<Object>{
new Map<String, Object>{
'ContactID' => contactId,
'PaymentTerms' => new Map<String, Object>{
'Bills' => new Map<String, Object>(paymentTerm),
'Sales' => new Map<String, Object>(paymentTerm)
}
}
}
};
HttpRequest req = new HttpRequest();
req.setEndpoint(
'callout:' + namedCredentialName + '/api.xro/2.0/Contacts'
);
req.setMethod('POST');
req.setHeader('xero-tenant-id', tenantId);
req.setHeader('Accept', 'application/json');
req.setHeader('Content-Type', 'application/json');
req.setBody(JSON.serialize(payload));
HttpResponse res = new Http().send(req);
System.debug(LoggingLevel.INFO, 'Xero status: ' + res.getStatusCode());
System.debug(LoggingLevel.INFO, 'Xero response: ' + res.getBody());
if (res.getStatusCode() < 200 || res.getStatusCode() >= 300) {
throw new CalloutException(
'Xero contact update failed: ' +
res.getStatusCode() + ' ' + res.getBody()
);
}
}
更新完成后,再次执行 GET /Contacts/{ContactID},确认 Xero 实际保存的 PaymentTerms.Sales。不要只根据 Salesforce 发出的 JSON 或 HTTP 200 判断更新结果。
如果 IsCustomer 已经是 true
如果查询结果明确显示:
"IsCustomer": true
但 Sales 仍未变化,联系人身份就不是原因。可以依次检查:
- POST 响应中返回的
PaymentTerms.Sales是否已经是新值。 - 更新与查询使用的
xero-tenant-id是否完全一致。 ContactID是否指向预期联系人,而不是联系人编号或其他 Salesforce 字段。terms.Day是否为整数,terms.Type是否为 Xero 接受的枚举值,例如DAYSAFTERBILLDATE。- 请求完成后,是否有其他同步任务、Xero App 或 Salesforce Flow 覆盖了该字段。
- 尝试只发送
Sales,检查问题是否与组合更新有关:
{
"Contacts": [
{
"ContactID": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"PaymentTerms": {
"Sales": {
"Day": 7,
"Type": "DAYSAFTERBILLDATE"
}
}
}
]
}
如果联系人已经是客户,单独更新 Sales 仍被静默忽略,响应中也没有 ValidationErrors,问题更可能与特定租户或 Xero 服务端行为有关。此时应保存完整响应、请求时间、tenant ID、ContactID,以及响应头中的请求或关联标识,然后提交给 Xero 支持核查。继续向 payload 添加未定义字段无法帮助定位问题。
备注:内容仅供参考。