从 Playbooks 进行收费和支付
本指南解释了如何在 Ansible playbooks 中实现收费和支付处理,以便于 OmniCRM 供应工作流。
📘 完整 API 参考:有关支付 API、钱包路由、退款和供应商无关架构的完整详细信息,请参见 支付系统 API 指南
概述
OmniCRM Ansible Playbooks 可以以多种不同方式处理支付,但所有这些方式都只是调用 CRM 的 API 来收费。
- 两阶段提交支付流程 - 对于需要立即支付授权和捕获的付费服务(即预付费服务)
- 直接交易创建 - 对于添加费用/信用而不需要立即支付处理(例如,设置费用、手动信用),这些费用可以在后续处理(即后付费服务)
这些方法确保原子交易,只有在供应成功时才会向客户收费,如果任何步骤失败,则会自动回滚。
定价灵活性:将 Playbooks 作为脚本引擎
重要: 在产品和服务中定义的美元价值(例如,retail_cost、wholesale_cost、retail_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
}
}
发生的事情:
- 卡捕获短缺金额(如果卡已被授权)
- **钱包记���**捕获的卡金额
- 钱包扣款全额服务金额
- 如果在授权元数据中为
invoice: true:- 借记交易创建(正金额 = 收费)
- 发票创建并链接到借记交易
- 贷记交易创建(负金额 = 收到的付款)
- 发票标记为已支付(借记和贷记净额为零)
- 交易出现在客户账单中
- 数据库中创建支付记录
- 如果
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": "授权已释放"
}
发生的事情:
- 卡授权已释放(如果卡已被授权)
- ���留的资金返回到客户的可用信用中
- 钱包未扣款(因为扣款仅在捕获时发生)
- 客户未收费
- 不会创建任何交易
注意:使用钱包优先路由时,无需退款,因为钱包在捕获时才会扣款。
直接交易创建
对于不需要支付处理的费用(设置费用、手动信用、调整),您可以通过 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:付费服务供应
对于需要立即支付的服务:
- 获取客户的支付方式
- 授权支付
- 在 OCS/CGRateS 中供应服务
- 在 CRM 中创建服务记录
- 捕获支付
- 失败时:释放授权并回滚
请参阅 play_topup_charge_then_action.yaml 获取完整示例。
模式 2:免费服务与设置费用
对于免费的服务但有一次性设置费用:
- 供应服务
- 创建服务记录
- 直接添加设置费用交易(无需支付处理)
- 设置费用出现在下一个发票中
请参阅 play_simple_service.yaml 的第 202-232 行获取完整示例。
模式 3:免费充值/附加服务
对于不需要支付的免费充值:
- 获取服务信息
- 执行 CGRateS 操作
- 更新服务日期
- 无需支付或交易创建
模式 4:通过 ActionPlan 进行定期收费
对于自动定期收费:
- 创建带有
*http_post的操作,指向供应端点 - 创建带有
*monthly定时的 ActionPlan - 将 ActionPlan 分配给账户
- CGRateS 将每月自动调用该端点
- 端点 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- 供应系统概述