跳到主要内容

从 Playbooks 进行收费和支付

本指南解释了如何在 Ansible playbooks 中实现收费和支付处理,以便于 OmniCRM 供应工作流。

📘 完整 API 参考:有关支付 API、钱包路由、退款和供应商无关架构的完整详细信息,请参见 支付系统 API 指南

概述

OmniCRM Ansible Playbooks 可以以多种不同方式处理支付,但所有这些方式都只是调用 CRM 的 API 来收费。

  1. 两阶段提交支付流程 - 对于需要立即支付授权和捕获的付费服务(即预付费服务)
  2. 直接交易创建 - 对于添加费用/信用而不需要立即支付处理(例如,设置费用、手动信用),这些费用可以在后续处理(即后付费服务)

这些方法确保原子交易,只有在供应成功时才会向客户收费,如果任何步骤失败,则会自动回滚。

定价灵活性:将 Playbooks 作为脚本引擎

重要: 在产品和服务中定义的美元价值(例如,retail_costwholesale_costretail_setup_cost)仅仅是存储在数据库中�� 默认值。它们并不决定您必须收费的金额——playbook 完全控制最终定价。

Ansible playbook 本质上是一个 脚本引擎,您可以根据任何业务逻辑需求进行定制。您可以:

  • 按原样使用存储的价格 - 在您的授权中简单引用 api_response_product.json.retail_cost
  • 完全覆盖价格 - 无论产品定义中是什么,都收取不同的金额
  • 应用动态折扣 - 根据客户等级、促销或忠诚度计算折扣百分比
  • 实施分层定价 - 根据数量、使用水平或合同条款收取不同的费率
  • 捆绑定价 - 将多个产品与自定义捆绑折扣组合
  • 基于时间的定价 - 根据一天中的时间、季节或促销期调整价格
  • 客户特定定价 - 从客户合同中查找谈判的费率

示例:覆盖产品价格

# 产品 retail_cost 为 $99.00,但我们想以 $79.00 收费以进行促销
- name: 设置促销价格(忽略产品的 retail_cost)
set_fact:
charge_amount: 79.00

- name: 授权促销支付
uri:
url: "http://localhost:5000/crm/payments/authorize/hold"
method: POST
body_format: json
body:
customer_id: "{{ customer_id | int }}"
amount: "{{ charge_amount | float }}" # 使用我们的覆盖,而不是 retail_cost
# ...

示例:客户等级折扣

# 根据客户等级应用折扣
- name: 获取客户等级
uri:
url: "http://localhost:5000/crm/customer/{{ customer_id }}"
# ...
register: customer_info

- name: 计算等级折扣
set_fact:
base_price: "{{ api_response_product.json.retail_cost | float }}"
discount_percent: >-
{% if customer_info.json.tier == 'gold' %}20
{% elif customer_info.json.tier == 'silver' %}10
{% else %}0{% endif %}

- name: 将折扣应用于最终价格
set_fact:
final_price: "{{ (base_price | float) * (1 - (discount_percent | float / 100)) | round(2) }}"

示例:批发合作伙伴定价

# 批发合作伙伴支付 wholesale_cost 而不是 retail_cost
- name: 根据客户类型确定价格
set_fact:
charge_amount: >-
{% if customer_info.json.is_wholesale_partner %}
{{ api_response_product.json.wholesale_cost | float }}
{% else %}
{{ api_response_product.json.retail_cost | float }}
{% endif %}

关键点是 您并不受限于任何定价模型。playbook 赋予您完全的编程控制,以实施您的组织所需的任何业务规则。CRM 中的产品/服务价格只是方便的默认值,playbooks 可以根据需要使用或忽略这些值。

两阶段提交支���流程

当您需要在供应过程中向客户的支付方式收费时,使用两阶段提交模式。这确保只有在供应成功完成时,客户才会被收费。

流程概述

实现模式

该模式遵循以下阶段:

阶段 1:授权 - 在支付方式上保留资金
阶段 2:供应 - 执行 OCS 余额/操作更新
阶段 3:捕获 - 在成功供应后完成支付
回滚 - 如果供应失败则释放授权

完整示例

以下是来自 play_topup_charge_then_action.yaml 的完整示例:

- name: Play Topup - Charge card then action the Topup
hosts: localhost
gather_facts: no
become: False

tasks:
- name: 包含 crm_config 的变量
ansible.builtin.include_vars:
file: "../../crm_config.yaml"
name: crm_config

# 获取产品和服务信息
- name: 从 CRM API 获取产品信息
uri:
url: "http://localhost:5000/crm/product/product_id//{{ product_id }}"
method: GET
headers:
Authorization: "Bearer {{ access_token }}"
return_content: yes
validate_certs: no
register: api_response_product

- name: 从 CRM API 获取服务信息
uri:
url: "http://localhost:5000/crm/service/service_id/{{ service_id }}"
method: GET
headers:
Authorization: "Bearer {{ access_token }}"
return_content: yes
validate_certs: no
register: api_response_service

- name: 设置服务和套餐事实
set_fact:
service_uuid: "{{ api_response_service.json.service_uuid }}"
customer_id: "{{ api_response_service.json.customer_id }}"
package_name: "{{ api_response_product.json.product_name }}"
monthly_cost: "{{ api_response_product.json.retail_cost }}"
wholesale_cost: "{{ api_response_product.json.wholesale_cost }}"

# 获取客户的默认支付方式
- name: 从支付控制器获取客户支付方式
uri:
url: "http://localhost:5000/crm/payments/methods?customer_id={{ customer_id }}"
method: GET
headers:
Authorization: "Bearer {{ access_token }}"
return_content: yes
validate_certs: no
register: api_response_payment_methods

- name: 从响应中获取默认 payment_method_id
set_fact:
payment_method_id: "{{ api_response_payment_methods.json | json_query(query) }}"
vars:
query: "data[?is_default==`true`].payment_method_id | [0]"

# ============================================================
# 两阶段提交支付流程
# ============================================================

- name: "阶段 1:授权支付(保留资金)"
uri:
url: "http://localhost:5000/crm/payments/authorize/hold"
method: POST
headers:
Content-Type: "application/json"
Authorization: "Bearer {{ access_token }}"
body_format: json
body:
{
"customer_id": "{{ customer_id | int }}",
"amount": "{{ monthly_cost | float }}",
"currency": "{{ crm_config.currency | default('AUD') }}",
"payment_method_id": "{{ payment_method_id | int }}",
"metadata": {
"description": "{{ package_name }} on {{ api_response_service.json.service_name }}",
"service_id": "{{ api_response_service.json.service_id | int }}",
"site_id": "{{ api_response_service.json.site_id | int }}",
"product_id": "{{ product_id }}",
"user_id": "{{ (initiating_user | int) if (initiating_user is defined and initiating_user is not none) else omit }}",
"title": "{{ package_name }}",
"wholesale_cost": "{{ wholesale_cost | float }}",
"invoice": true,
"contract_days": "{{ days | int }}",
"send_email": true
}
}
return_content: yes
register: api_response_authorization

- name: 确保授权成功
assert:
that:
- api_response_authorization.status == 200
- api_response_authorization.json.success == true

- name: 存储 authorization_id 以供捕获/释放
set_fact:
authorization_id: "{{ api_response_authorization.json.data.authorization_id }}"

# 阶段 2:OCS 供应(包装在块中以便于事务回滚)
- block:
- name: "阶段 2:执行 CGRateS 操作"
uri:
url: "http://{{ crm_config.ocs.cgrates }}/jsonrpc"
method: POST
body_format: json
headers:
Content-Type: "application/json"
Authorization: "Bearer {{ access_token }}"
body:
{
"id": "{{ 999999999 | random }}",
"method": "APIerSv1.ExecuteAction",
"params": [{
"Tenant": "{{ crm_config.ocs.ocsTenant }}",
"Account": "{{ service_uuid }}",
"ActionsId": "{{ cgr_action_name }}"
}]
}
status_code: 200
register: action_execute_response

- name: 确保操作的响应是 OK
assert:
that:
- action_execute_response.status == 200
- action_execute_response.json.result == "OK"

# 阶段 3:支付捕获 - 在成功供应后完成交易
- name: "阶段 3:捕获授权支付"
uri:
url: "http://localhost:5000/crm/payments/capture/{{ authorization_id }}"
method: POST
headers:
Content-Type: "application/json"
Authorization: "Bearer {{ access_token }}"
body_format: json
body:
{
"metadata": {
"provisioning_status": "success",
"cgr_action": "{{ cgr_action_name }}"
}
}
return_content: yes
register: api_response_capture

- name: 确保捕获成功
assert:
that:
- api_response_capture.status == 200
- api_response_capture.json.success == true

rescue:
# 事务回滚:作废授权以释放保留的资金
- name: "回滚:释放支付授权"
uri:
url: "http://localhost:5000/crm/payments/release/{{ authorization_id }}"
method: POST
headers:
Content-Type: "application/json"
Authorization: "Bearer {{ access_token }}"
body_format: json
body:
{
"metadata": {
"release_reason": "provisioning_failed",
"cgr_action": "{{ cgr_action_name }}"
}
}
return_content: yes
register: api_response_release

- name: 在回滚后终止 playbook 执行
fail:
msg: "OCS 供应失败。支付授权 {{ authorization_id }} 已作废。客户未收费。"

支付 API 端点

💡 钱包优先路由:支付系统通过优先使用钱包余额自动优化卡收费。如果客户钱包中有 $1 并且购买了 $10 的附加服务,则仅向其卡收费 $9。有关详细信息,请参见 钱包优先路由部分

1. 授权/保留支付

端点: POST /crm/payments/authorize/hold

此端点对客户的支付方式进行指定金额的保留。资金被保留但尚未捕获。

钱包优先行为:授权在检查钱包余额后自动计算短缺。如果钱包余额为 $150,而授权为 $500,则仅在卡上授权 $350。钱包在捕获时扣款。

请求体:

{
"customer_id": 123,
"amount": 49.99,
"currency": "AUD",
"payment_method_id": 456,
"metadata": {
"description": "套餐名称和描述",
"service_id": 789,
"site_id": 12,
"product_id": "34",
"user_id": 5,
"title": "套餐名称",
"wholesale_cost": 25.00,
"invoice": true,
"contract_days": 30,
"send_email": true
}
}

字段描述:

  • customer_id(必需) - 被收费的客户 ID
  • amount(必需) - 要授权的金额,采用小数格式
  • currency(可选) - 货币代码(默认为系统默认值)
  • payment_method_id(必需) - 要收费的支付方式
  • metadata(可选) - 交易创建的附加数据:
    • description - 交易描述
    • service_id - 被收费的服务
    • site_id - 与服务相关联的网站
    • product_id - 正在供应的产品
    • user_id - 发起收费的用户(可选)
    • title - 交易标题
    • wholesale_cost - 您的成本(用于利润跟踪)
    • invoice - 如果为 true,在捕获时自动创建交易
    • contract_days - 合同期限
    • send_email - 如果为 true,发送电子邮件通知

响应:

{
"success": true,
"message": "支付已授权(创建了保留)",
"data": {
"authorization_id": 301,
"vendor_authorization_id": "auth_xxxxx",
"amount": 500.00,
"currency": "USD",
"status": "authorized",
"wallet_balance": 150.00,
"wallet_to_use": 150.00,
"card_amount": 350.00,
"message": "卡已授权 $350(钱包充值)。在捕获时将发生 $500 的钱包扣款。"
}
}

重要:

  • 保存 authorization_id 以便在捕获或释放调用中使用
  • 注意 card_amount 仅显示短缺已被授权
  • 钱包余额已检查,但在捕获之前不会扣款

2. 捕获支付

端点: POST /crm/payments/capture/{authorization_id}

此端点完成支付授权并向客户收费。如果在授权元数据中设置了 invoice: true,则会自动创建交易。

请求体:

{
"metadata": {
"provisioning_status": "success",
"cgr_action": "Action_Topup_Standard",
"additional_info": "任何其他相关数据"
}
}

响应:

{
"success": true,
"data": {
"payment_id": "pay_xyz789",
"transaction_id": 1234
}
}

发生的事情:

  1. 卡捕获短缺金额(如果卡已被授权)
  2. **钱包记���**捕获的卡金额
  3. 钱包扣款全额服务金额
  4. 如果在授权元数据中为 invoice: true
    • 借记交易创建(正金额 = 收费)
    • 发票创建并链接到借记交易
    • 贷记交易创建(负金额 = 收到的付款)
    • 发票标记为已支付(借记和贷记净额为零)
    • 交易出现在客户账单中
  5. 数据库中创建支付记录
  6. 如果 send_email: true,客户将收到发票电子邮件

示例:$500 授权与 $150 钱包余额:

  • 卡捕获:$350
  • 钱包记入:+$350(钱包现在为 $500)
  • 钱包扣款:-$500(服务费用)
  • 最终钱包:$0

有关详细流程,请参见 钱包优先路由示例

3. 释放支付授权

端点: POST /crm/payments/release/{authorization_id}

此端点取消支付授权并释放保留的资金。在供应失败时使用此方法进行恢复/回滚场景。

请求体:

{
"metadata": {
"release_reason": "provisioning_failed",
"error_details": "OCS 账户创建失败"
}
}

响应:

{
"success": true,
"message": "授权已释放"
}

发生的事情:

  1. 卡授权已释放(如果卡已被授权)
  2. ���留的资金返回到客户的可用信用中
  3. 钱包未扣款(因为扣款仅在捕获时发生)
  4. 客户未收费
  5. 不会创建任何交易

注意:使用钱包优先路由时,无需退款,因为钱包在捕获时才会扣款。

直接交易创建

对于不需要支付处理的费用(设置费用、手动信用、调整),您可以通过 API 直接创建交易。

通过 API 添加交易

端点: PUT /crm/transaction/

来自 play_simple_service.yaml 的示例:

- name: 通过 API 添加设置费用交易
uri:
url: "http://localhost:5000/crm/transaction/"
method: PUT
headers:
Content-Type: "application/json"
Authorization: "Bearer {{ access_token }}"
body_format: json
body:
{
"customer_id": {{ customer_id | int }},
"service_id": {{ service_creation_response.json.service_id | int }},
"title": "{{ package_name }} - 设置费用",
"description": "为 {{ package_comment }} 的设置费用",
"invoice_id": null,
"wholesale_cost": {{ api_response_product.json.wholesale_setup_cost | float }},
"retail_cost": "{{ api_response_product.json.retail_setup_cost | float }}"
}
return_content: yes
register: api_response_transaction

- name: 确保交易响应正常
assert:
that:
- api_response_transaction.status == 200

请求体字段:

  • customer_id(必需) - 客户 ID
  • service_id(可选) - 用于链接交易的服务 ID
  • title(必需) - 短交易名称
  • description(可选) - 详细描述
  • invoice_id(可选) - 如果已开票,则为发票 ID(通常为 null)
  • wholesale_cost(可选) - 您的成本
  • retail_cost(必需) - 面向客户的成本
  • site_id(可选) - 用于链接交易的网站 ID
  • tax_percentage(可选) - 税率百分比

用例:

  • 服务供应期间的设置费用
  • 安装费用
  • 手动信用或调整
  • 设备费用
  • 行政费用

注意: 直接交易创建不会处理支付 - 它仅创建账单记录。该交易将显示为未开票,并可以包含在未来的发票中。

计算按比例收费

按比例收费允许您根据部分计费周期按比例向客户收费。这在以下情况下很常见:

  • 客户在中旬注册
  • 服务在周期中升级/降级
  • 根据使用天数计算账单

按比例计算公式

pro_rata_charge = (monthly_cost × days_remaining) / days_in_month

实现示例

以下是在 playbook 中计算按比例收费的方法:

# 计算��分月份的按比例收费
# 如果客户在 15 号注册,而账单在 1 号,
# 按剩余天数按比例收费

- name: 获取当前月份的天数
command: "date +%d"
register: current_day

- name: 获取当前月份的总天数
command: "date -d 'last day of this month' +%d"
register: days_in_month

- name: 获取月份的最后一天
command: "date -d 'last day of this month' +%Y-%m-%d"
register: last_day_of_month

- name: 计算本月剩余天数
set_fact:
days_remaining: "{{ (days_in_month.stdout | int) - (current_day.stdout | int) + 1 }}"

- name: 计算按比例费用
set_fact:
pro_rata_cost: "{{ ((monthly_cost | float) * (days_remaining | float) / (days_in_month.stdout | float)) | round(2) }}"

- name: 显示计算细节
debug:
msg:
- "每月费用:${{ monthly_cost }}"
- "本月天数:{{ days_in_month.stdout }}"
- "剩余天数:{{ days_remaining }}"
- "按比例收费:${{ pro_rata_cost }}"

# 在授权或交易创建中使用 pro_rata_cost
- name: "授权按比例支付"
uri:
url: "http://localhost:5000/crm/payments/authorize/hold"
method: POST
headers:
Content-Type: "application/json"
Authorization: "Bearer {{ access_token }}"
body_format: json
body:
{
"customer_id": "{{ customer_id | int }}",
"amount": "{{ pro_rata_cost | float }}",
"currency": "{{ crm_config.currency | default('AUD') }}",
"payment_method_id": "{{ payment_method_id | int }}",
"metadata": {
"title": "{{ package_name }} (按比例 {{ days_remaining }} 天)",
"description": "为 {{ days_remaining }}/{{ days_in_month.stdout }} 天的 {{ package_name }} 收取按比例费用",
"service_id": "{{ service_id | int }}",
"invoice": true
}
}

从自定义开始日期计算按比例

如果您需要从特定开始日期到账单日期计算按比例:

- name: 设置自定义开始日期和账单日期
set_fact:
service_start_date: "2024-01-15"
next_billing_date: "2024-02-01"

- name: 计算日期之间的天数
shell: |
echo $(( ( $(date -d "{{ next_billing_date }}" +%s) - $(date -d "{{ service_start_date }}" +%s) ) / 86400 ))
register: days_until_billing

- name: 获取计费周期的天数(通常为 30)
set_fact:
billing_period_days: 30

- name: 计算按比例费用
set_fact:
pro_rata_cost: "{{ ((monthly_cost | float) * (days_until_billing.stdout | float) / (billing_period_days | float)) | round(2) }}"

- name: 显示计算
debug:
msg:
- "开始日期:{{ service_start_date }}"
- "下一个账单:{{ next_billing_date }}"
- "距离账单的天数:{{ days_until_billing.stdout }}"
- "按比例收费:${{ pro_rata_cost }}"

按比例示例场景

场景 1:中旬注册

  • 客户在 1 月 15 日注册
  • 每月费用:$60.00
  • 1 月的天数:31
  • 剩余天数:17(包括 15 日到 31 日)
  • 按比例收费:$60.00 × 17 ÷ 31 = $32.90

场景 2:服务升级

  • 客户在计费周期的第 10 天升级
  • 旧计划:$30/月
  • 新计划:$50/月
  • 周期中的天数:30
  • 剩余天数:21
  • 差额:$20/月
  • 按比例收费:$20.00 × 21 ÷ 30 = $14.00

最佳实践

1. 始终使用块/恢复模式

将支付捕获和供应包装在块/恢复中,以确保回滚:

- block:
# 供应任务
- name: 供应服务
uri: ...

# 仅在成功后捕获支付
- name: 捕获支付
uri: ...

rescue:
# 如果任何操作失败,则释放授权
- name: 释放支付授权
uri: ...

- name: 失败 playbook
fail:
msg: "供应失败,客户未收费"

2. 验证所有 API 响应

始终确保关键操作成功:

- name: 授权支付
uri: ...
register: api_response_authorization

- name: 确保授权成功
assert:
that:
- api_response_authorization.status == 200
- api_response_authorization.json.success == true
fail_msg: "支付授权失败:{{ api_response_authorization.json }}"

3. 存储授权 ID

始终保存 authorization_id 以供捕获/释放使用:

- name: 存储 authorization_id 以供捕获/释放
set_fact:
authorization_id: "{{ api_response_authorization.json.data.authorization_id }}"

4. 有效使用元数据

在授权请求中包含全面的元数据:

metadata:
description: "清晰描述客户收费内容"
service_id: "{{ service_id | int }}"
product_id: "{{ product_id }}"
user_id: "{{ initiating_user | int }}"
title: "交易的简短标题"
wholesale_cost: "{{ wholesale_cost | float }}"
invoice: true # 在捕获时自动创建交易
send_email: true # 发送客户通知

5. 四舍五入货币值

始终将货币计算四舍五入到小数点后两位:

- name: 计算带四舍五入的费用
set_fact:
final_cost: "{{ (base_cost | float * multiplier | float) | round(2) }}"

6. 处理缺失的支付方式

在尝试授权之前检查客户是否具有默认支付方式:

- name: 获取默认支��方式
set_fact:
payment_method_id: "{{ api_response_payment_methods.json | json_query(query) }}"
vars:
query: "data[?is_default==`true`].payment_method_id | [0]"

- name: 验证支付方式是否存在
assert:
that:
- payment_method_id is defined
- payment_method_id != ""
- payment_method_id != None
fail_msg: "未找到客户 {{ customer_id }} 的默认支付方式"

常见模式

模式 1:付费服务供应

对于需要立即支付的服务:

  1. 获取客户的支付方式
  2. 授权支付
  3. 在 OCS/CGRateS 中供应服务
  4. 在 CRM 中创建服务记录
  5. 捕获支付
  6. 失败时:释放授权并回滚

请参阅 play_topup_charge_then_action.yaml 获取完整示例。

模式 2:免费服务与设置费用

对于免费的服务但有一次性设置费用:

  1. 供应服务
  2. 创建服务记录
  3. 直接添加设置费用交易(无需支付处理)
  4. 设置费用出现在下一个发票中

请参阅 play_simple_service.yaml 的第 202-232 行获取完整示例。

模式 3:免费充值/附加服务

对于不需要支付的免费充值:

  1. 获取服务信息
  2. 执行 CGRateS 操作
  3. 更新服务日期
  4. 无需支付或交易创建

模式 4:通过 ActionPlan 进行定期收费

对于自动定期收费:

  1. 创建带有 *http_post 的操作,指向供应端点
  2. 创建带有 *monthly 定时的 ActionPlan
  3. 将 ActionPlan 分配给账户
  4. CGRateS 将每月自动调用该端点
  5. 端点 playbook 处理支付处理

故障排除

授权失败

症状: 授权端点返回错误

常见原因:

  • 支付方式不存在或无效
  • 资金不足
  • 支付方式过期
  • 客户 ID 不匹配

解决方案: 检查支付方式状态和客户余额。

在成功供应后捕获失败

症状: 服务已供应但支付捕获失败

问题: 这是一个关键失败状态 - 服务处于活动状态但客户未收费

解决方案:

  • 授权可能已过期(通常为 7 天)
  • 在尝试捕获之前检查授权是否仍然有效
  • 实施失败捕获的监控
  • 可能需要手动干预

捕获后未创建交易

症状: 支付已捕获但账单中没有交易

原因: 在授权元数据中未设置 invoice: true

解决方案: 或者:

  • 在授权元数据中设置 invoice: true,或者
  • 在成功捕获后手动创建交易

按比例计算不正确

症状: 按比例收费与预期值不匹配

常见问题:

  • 在天数计算中出现错误(包括/排除开始/结束日期)
  • 使用了错误��月份进行天数计算
  • 四舍五入错误

解决方案:

  • 使用包含的日期范围(包括开始和结束天数)
  • 始终四舍五入到小数点后两位
  • 使用已知值测试计算
  • 记录计算中包含的日期

退款和错误处理

退款选项

支付系统支持两种类型的退款:

1. 退款到支付来源 - 资金退回到原始卡/PayPal

- name: 退款到客户的卡
uri:
url: "http://localhost:5000/crm/payments/refund"
method: POST
headers:
Content-Type: "application/json"
Authorization: "Bearer {{ access_token }}"
body_format: json
body:
{
"transaction_id": "{{ vendor_transaction_id }}",
"vendor": "stripe",
"amount": "{{ refund_amount | float }}",
"reason": "customer_request"
}

2. 退款到钱包 - 立即为未来的购买提供余额(在错误场景中自动处理)

对于供应失败,系统自动将钱包记入,而不是退款到卡,以:

  • 避免退款费用
  • 提供立即可用性以进行重试
  • 改善客户体验

有关完整详细信息,请参见 退款选项

供应商支持

支付系统是 与供应商无关的,目前支持:

  • ✅ Stripe(卡,ACH)
  • ✅ PayPal(PayPal 账户,卡)

可以在不更改 playbooks 的情况下添加新的支付供应商(Square、Adyen、Braintree 等)。

另请参见:

相关文档

特定于 Playbook 的指南

  • concepts_ansible.md - 一般 playbook 模式和结构
  • concepts_provisioning.md - 供应系统概述

支付系统文档