You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Xero Contacts API仅更新Bills付款条款,忽略Sales条款且无报错

解决Xero Contacts API更新联系人Sales付款条款无效的问题

我正在开发Salesforce到Xero的集成,通过Xero Contacts API(POST /api.xro/2.0/Contacts)更新联系人付款条款。发送的请求Payload同时包含Bills和Sales付款条款,请求返回HTTP 200成功,联系人的采购默认值(Bills)在Xero中更新正确,但销售默认值(Sales)未发生变化,且响应中无任何错误或验证提示。请问是否有人成功通过该接口同时更新联系人的Bills和Sales付款条款?

请求Payload示例

{
  "Contacts": [
    {
      "ContactID": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
      "PaymentTerms": {
        "Bills": { "Day": 7, "Type": "DAYSAFTERBILLDATE" },
        "Sales": { "Day": 7, "Type": "DAYSAFTERBILLDATE" }
      }
    }
  ]
}

完整Apex代码

public class XeroAPIHandler {

    public class AccountInput {
        @InvocableVariable(required=true)
        public Id accountId;
    }

    @InvocableMethod(label='Update Xero Payment Terms' description='Updates Xero contact with payment terms based on Account field')
    public static void updatePaymentTermsFromFlow(List<AccountInput> inputList) {
        for (AccountInput input : inputList) {
            List<Account> acc = [SELECT Id, Name, Xero_Account_Number__c, Payment_Terms__c, Account_State__c
                                 FROM Account
                                 WHERE Id = :input.accountId
                                 LIMIT 1];
            if (acc.size() > 0) {
                String tenantId = tenantIdForState(acc[0].Account_State__c);
                String namedCredentialName = namedCredForState(acc[0].Account_State__c);
                updatePaymentTerms(acc[0].Xero_Account_Number__c, acc[0].Payment_Terms__c, tenantId, namedCredentialName); 
            }
        }
    }

    public static String tenantIdForState(String state) {
        if (String.isBlank(state)) return null;
        switch on state {
            when 'ABC' { return Label.Xero_Tenant_Id_ABC; }
            when 'DEF' { return Label.Xero_Tenant_Id_DEF; }
            when 'GHI' { return Label.Xero_Tenant_Id_GHI; }
            when 'JK'  { return Label.Xero_Tenant_Id_JK;  }
            when else  { return null; }
        }
    }

    public static String namedCredForState(String state) {
        if (String.isBlank(state)) return null;
        switch on state {
            when 'ABC' { return 'Xero_ABC'; }
            when 'DEF' { return 'Xero_DEF'; }
            when 'GHI' { return 'Xero_GHI'; }
            when 'JK'  { return 'Xero_JK';  }
            when else  { return null; }
        }
    }

    @future(callout=true)
    public static void updatePaymentTerms(String AccountNumber, String term, String tenantId, String namedCredentialName) {
        String xeroContactId = AccountNumber;
        if (String.isBlank(xeroContactId)) return;

        XeroPaymentTermMapper.XeroPaymentTerm terms = XeroPaymentTermMapper.mapPaymentTerm(term);
        if (terms == null) return;

        Map<String, Object> payload = new Map<String, Object>{
            'Contacts' => new List<Object>{
                new Map<String, Object>{
                    'ContactID' => xeroContactId,
                    'PaymentTerms' => new Map<String, Object>{
                        'Bills' => new Map<String, Object>{
                            'Day' => terms.Day,
                            'Type' => terms.Type
                        },
                        'Sales' => new Map<String, Object>{
                            'Day' => terms.Day,
                            'Type' => terms.Type
                        }
                    }
                }
            }
        };

        HttpRequest req = new HttpRequest();
        req.setEndpoint('callout:' + namedCredentialName + '/api.xro/2.0/Contacts');
        req.setHeader('xero-tenant-id', tenantId);
        req.setMethod('POST');
        req.setHeader('Content-Type', 'application/json');
        req.setBody(JSON.serialize(payload));

        Http http = new Http();
        try {
            HttpResponse res = http.send(req);
            System.debug('Xero response: ' + res.getBody());
        } catch (Exception e) {
            System.debug('Xero callout failed: ' + e.getMessage());
        }
    }
}

可能的解决方案

1. 确认Xero API权限范围

确保连接Xero的应用已获得contacts的读写权限,部分权限配置可能仅允许更新采购相关字段,需检查应用权限设置中是否包含完整的联系人字段修改权限。

2. 验证字段命名的准确性

Xero API对字段大小写敏感,确认Sales字段的拼写和嵌套结构完全符合官方文档要求。可以尝试将Sales替换为SalesTerms(部分旧版API或特定场景下的字段名差异),或者直接核对Xero Contacts API的最新字段定义。

3. 检查联系人类型与默认设置

  • 如果该联系人在Xero中被标记为仅供应商,则销售相关字段(包括Sales付款条款)可能无法被更新,需确认联系人的类型设置为“客户/供应商”或“客户”。
  • 检查Xero中该联系人是否存在销售默认值的全局覆盖设置,部分租户级配置可能会限制联系人级别的销售条款修改。

4. 调整请求方法或端点

虽然POST方法用于创建/更新联系人,但部分场景下使用PUT /api.xro/2.0/Contacts/{ContactID}进行单联系人更新可能更可靠,尝试切换为PUT请求并简化Payload,仅包含需要更新的Sales付款条款字段,测试是否生效。

5. 深入调试响应内容

当前代码仅打印响应Body,建议添加对响应Header的调试,同时检查返回的Contacts对象中Sales付款条款是否被正确返回:

// 修改调试代码
System.debug('Xero response status: ' + res.getStatusCode());
System.debug('Xero response headers: ' + res.getHeaders());
System.debug('Xero response body: ' + res.getBody());

// 解析响应中的Contact对象,查看Sales字段是否被正确更新
Map<String, Object> responseMap = (Map<String, Object>)JSON.deserializeUntyped(res.getBody());
List<Object> contacts = (List<Object>)responseMap.get('Contacts');
if(contacts != null && contacts.size() > 0){
    Map<String, Object> contact = (Map<String, Object>)contacts[0];
    Map<String, Object> paymentTerms = (Map<String, Object>)contact.get('PaymentTerms');
    if(paymentTerms != null){
        System.debug('Response Sales Terms: ' + paymentTerms.get('Sales'));
    }
}

如果响应中Sales字段未被返回或值不正确,说明Xero API未处理该字段的更新请求,需进一步排查字段权限或API版本问题。

6. 确认API版本兼容性

当前使用的是Xero API 2.0,检查是否存在版本差异导致Sales字段不被支持,尝试升级到最新的API版本(如2.1),或查阅Xero API的版本变更日志,确认PaymentTerms.Sales字段的支持情况。

内容的提问来源于stack exchange,提问作者Ob1Newb

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.12 02:54:51