remp/crm-payments-module


README

Translation status @ Weblate

安装模块

我们建议使用Composer进行安装和更新管理。

composer require remp/crm-payments-module

启用模块

将已安装的扩展添加到您的 app/config/config.neon 文件中。

extensions:
	- Crm\PaymentsModule\DI\PaymentsModuleExtension

将以下命令添加到您的计划任务(例如 crontab)中,并更改路径以匹配您的部署路径

# charge available recurrent payments
*/15 * * * * flock /tmp/payments_charge.lock /usr/bin/php /var/www/html/bin/command.php payments:charge

# pre-calculate payment-related metadata
04 04 * * * /usr/bin/php /var/www/html/bin/command.php payments:calculate_averages

配置

数据保留配置

您可以通过在项目配置文件中使用(例如)来配置在哪个时间之前 application:cleanup 删除旧仓库数据和它所使用的列

services:
    paymentLogsRepository:
        setup:
            - setRetentionThreshold('-2 months', 'created_at')

快速充电检查配置

您可以通过添加以下内容到您的配置来配置快速充电阈值检查

payments:
    fastcharge_threshold: 24 # default: 24; number of hours (if set to 0 fast charge check is disabled)

快速充电检查是通过 RecurrentPaymentsChargeCommand::validateRecurrentPayment 完成的,并在充电过程中发生错误时防止系统重复充电。

计划命令

为了使支付模块正确运行,请将以下命令的执行添加到您的计划任务中。示例显示使用crontab执行的情况(更改路径以匹配您的部署路径)

# calculate payment related averages; expensive calculations that should be done nightly
04 04 * * * php /var/www/html/bin/command.php payments:calculate_averages

# recurrent payment charges; using flock to allow only single instance running at once
*/15 * * * * flock /tmp/payments_charge.lock /usr/bin/php /var/www/html/bin/command.php payments:charge

### OPTIONAL 

# failcheck to prevent payments not working without anyone noticing (see command options) 
*/10 * * * * php /var/www/html/bin/command.php payments:last_payments_check --notify=admin@example.com

# try to acquire debit card expiration dates for cards that don't have it
*/10 * * * * php /var/www/html/bin/command.php payments:update_recurrent_payments_expires

# stop recurrent payments with expired cards
7 2 1 * * php /var/www/html/bin/command.php payments:stop_expired_recurrent_payments

# if you use Cardpay/Comfortpay gateways and bank sends you email notifications, you can confirm payments based
# on those emails
*/3 * * * * php /var/www/html/bin/command.php payments:tatra_banka_mail_confirmation

服务命令

模块可能提供在部署环境中运行的命令。主要用于处理内部更改和防止直接与数据库交互。您可以通过在运行命令时使用--help开关来显示所需和可选参数。

支付模块不提供服务命令。

支付网关

模块有一组默认支持的支付网关,我们开发和使用了这些网关

  • free。为开发目的开发,用于测试与支付相关的流程。
  • bank_transfer。网关生成未完成的支付,并显示用户银行账户、金额和交易识别码,以便稍后可以配对和确认支付。
  • cardpay (tatrabanka.sk)。斯洛伐克银行提供的单次卡支付。
  • comfortpay (tatrabanka.sk)。斯洛伐克银行提供的定期卡支付(CRM处理收费)
  • csob (csob.cz)。捷克银行提供的单次卡支付。
  • csob_one_click (csob.cz)。捷克银行提供的定期卡支付(CRM处理收费)
  • paypal (paypal.com)。由主要全球提供商提供的单次支付。
  • paypal_reference (paypal.com)。由主要全球提供商提供的定期支付(CRM处理收费)
  • tatrapay (tatrabanka.sk)。与斯洛伐克银行网上银行链接的单次支付。

默认情况下,只有bank_transfer作为默认支付网关由 PaymentsModule 启用。您可以通过将以下片段添加到您的 app/config/config.neon 来启用您希望使用的网关

services:
	# ...
	gatewayFactory:
		setup:
			- registerGateway(free, Crm\PaymentsModule\Gateways\Free)

目前,有几种网关实现可以作为单独的模块添加到您的 CRM 安装中

  • stripe (stripe.com)。由主要全球提供商提供的单次支付。
  • stripe_recurrent (stripe.com)。由主要全球提供商提供的定期支付(CRM处理收费)
  • slsp_sporopay (slsp.sk)。与斯洛伐克银行网上银行链接的单次支付。
  • vub_eplatby (vub.sk)。与斯洛伐克银行网上银行链接的单次支付。

标准(单次)支付

标准和初始定期支付有共同的处理开始。一旦系统生成新支付实例,用户可以重定向到支付网关进行处理。每个网关都需要提供不同的参数集,因此网关负责生成包含所有必需参数的重定向URL。

由于 remp/crm-payments-module 仅负责实际支付处理,前端流程可以通过我们的 remp/crm-salesfunnel-module 进行管理,该模块提供创建销售漏斗(支付窗口)的方式,汇总统计数据,并在支付后显示用户成功页面,还可以通过小部件扩展。

支付后,用户将被重定向回CRM。每个网关都提供自己的URL,用户在支付完成处理时将被重定向。

如果支付成功,支付模块使用 PaymentCompleteRedirectManager 来确定用户应看到哪种成功页面。如果使用 crm-salesfunnel-module,则用户将被重定向到模块注册的成功页面。

支付处理流程可以用以下图表描述

Payment processing diagram

周期性支付

启动支付

如果支付使用支持周期性支付的网关,则初始流程通常与常规支付相同。区别在于处理成功初始支付的过程中。

支付模块创建一个新的 周期性支付 实例——一个定义系统何时再次向用户收费以及用户在下次收费时将获得什么 订阅类型 的配置文件。

每个 周期性支付 实例代表一个将来将被收取的单次支付。这意味着,如果收费失败,系统将根据重试规则计算出新的收费日期,并将失败信息存储到原始的周期性支付中。同样,如果收费成功,将创建新的订阅,并为下一个周期定义新的 周期性支付 以进行收费。这样,系统就能够提供有关整个用户收费历史的每次收费尝试的信息,包括银行批准/失败代码。

所有这些都是在后端完成的,无需系统要求任何用户交互。此部分仅解释流程并描述术语,以便在CRM管理员界面显示时,读者能够理解显示的数据。

自动收费

要向用户收费,请将 payments:charge 命令添加到您的调度程序中。命令不处理并发运行 - 这意味着阻止多个重叠的命令实例同时运行的职责由您的调度程序承担。否则,用户在同一期间可能会被收取两次费用。

我们建议使用 flock 或其他锁定工具来防止在先前的实例仍在运行时执行命令。以下是一个用于每15分钟运行一次的 crontab 的示例片段

*/15 * * * * flock /tmp/payments_charge.lock /usr/bin/php /var/www/html/bin/command.php payments:charge

通知过期卡片

如果网关支持,CRM将获取用于执行周期性收费的每个cid(实际上是信用卡)的过期日期。可选地,您可以在调度程序中添加命令,以自动停止过期的周期性支付

# stop recurrent payments with expired cards
7 2 1 * * php /var/www/html/bin/command.php payments:stop_expired_recurrent_payments

当周期性支付停止时,将发出 Crm\PaymentsModule\Events\RecurrentPaymentCardExpiredEvent。默认情况下,PaymentsModule 检查用户是否有其他活动的周期性支付。如果没有,它将发送带有 card_expires_this_month 模板代码的 NotificationEvent

如果您对默认实现不满意,您可以通过在模块定义中取消注册来移除默认处理程序

class FooModule extends Crm\ApplicationModule\CrmModule
{
    // ...
    public function registerEventHandlers(League\Event\Emitter $emitter)
    {
        $emitter->removeListener(
            \Crm\PaymentsModule\Events\RecurrentPaymentCardExpiredEvent::class,
            $this->getInstance(\Crm\PaymentsModule\Events\RecurrentPaymentCardExpiredEventHandler::class)
        );
        // ...
    }
}

实现新的网关

如果需要,您可以实现并将新网关集成到CRM中。根据您是实施标准网关还是周期性网关,您的实现应实现 Crm\PaymentsModule\Gateways\PaymentInterface 或前者以及 Crm\PaymentsModule\Gateways\RecurrentPaymentInterface

在实现网关时,我们建议扩展 Crm\PaymentsModule\Gateways\GatewayAbstract,以避免实现始终相似的部分,从而避免代码重复。

一旦准备好实现,您需要在自己的模块中的种子文件中将它初始化到数据库中(例如查看PaymentGatewaysSeeder),并将其注册到应用程序的配置中。

services:
	# ...
	- Crm\FooModule\Gateways\Foo
	# ...
	gatewayFactory:
		setup:
			- registerGateway(foo, Crm\FooModule\Gateways\Foo)

然后,添加一个将网关插入数据库的 种子器。请参阅Crm\PaymentsModule\Seeders\PaymentGatewaysSeeder作为示例 种子器 实现,并在CRM框架文档的注册种子器部分查看如何在您的模块中注册种子器。

添加支付完成重定向解析器

如果您想根据任意规则更改支付后显示给用户的成功页面——例如,您的网关可能希望用户看到一些特殊的优惠或要求其输入一些额外数据——您可以注册 重定向解析器 来处理此请求。当支付被确认时,重定向解析器将决定(基于注册解析器的优先级)是重定向用户到特殊成功页面,还是默认成功页面(如果使用remp/crm-salesfunnel-module)就足够了。

重定向解析器的实现可能如下所示

<?php

namespace Crm\FooModule\Model;

use Crm\PaymentsModule\Model\PaymentCompleteRedirectResolver;
use Nette\Database\Table\ActiveRow;

class FooPaymentCompleteRedirectResolver implements PaymentCompleteRedirectResolver
{
    public function wantsToRedirect(?ActiveRow $payment, string $status): bool
    {
        if ($payment && $status === self::PAID) {
            return $payment->payment_gateway->code === 'foo_gateway';
        }
        return false;
    }

    public function redirectArgs(?ActiveRow $payment, string $status): array
    {
        if ($payment && $status === self::PAID) {
            return [
                ':Foo:SalesFunnel:Success',
                ['variableSymbol' => $payment->variable_symbol],
            ];
        }
        throw new \Exception('unhandled status when requesting redirectArgs (did you check wantsToRedirect first?): ' . $status);
    }
}

在示例中,解析器首先检查是否应该为这次特定的支付发生重定向——如果支付是通过foo_gateway完成的,则应该发生重定向。然后,redirectArgs方法返回一个参数数组,这些参数将1:1用于Nette的$this->redirect()调用。用户将被重定向到我们的FooModule中实现的SalesFunnelPresenterrenderSuccess方法,显示我们的自定义成功页面。

当实现就绪时,解析器需要在app/config/config.neon中注册,并指定解析器执行的顺序——数字越大,优先级越高。

services:
	# ...
	paymentCompleteRedirect:
	 	setup:
	 		- registerRedirectResolver(Crm\FooModule\Model\FooPaymentCompleteRedirectResolver(), 400)

银行转账落地页面

当使用bank_transfer时,用户不会被重定向到外部支付网关提供商,而是CRM显示用户应手动完成支付的信息。

默认情况下,这是由BankTransferPresenter处理的,但您可以使用自己的自定义屏幕来显示转账信息,方法是使用自己的重定向解析器而不是银行转账的默认解析器

创建解析器,并在您的config.neon中将其注册,优先级大于10。

services:
	# ...
	paymentCompleteRedirect:
		setup:
			- registerRedirectResolver(Crm\FooModule\BankTransferPaymentCompleteRedirectResolver(), 50)

银行电子邮件处理

有时用户在完成支付但尚未返回CRM进行内部支付确认之前就退出了支付流程。这种情况总是令人不快的,因为用户既没有钱也没有订阅。

为了支持这种情况,我们添加了读取银行确认电子邮件并尝试根据收到的电子邮件确认未完成的支付的功能。

该实现目前还不是通用的,您需要创建自己的命令来检查邮箱。请查看我们包含在这个包中的两个实现:针对Tatra银行CSOB的确认命令。

邮件下载器

默认情况下,我们的邮件处理命令(例如:塔特拉银行CSOB)使用我们实现的邮件下载器:ImapMailDownloader。如有需要,您可以替换此下载器,方法如下

  1. 创建自己的邮件下载器,该下载器必须实现MailDownloaderInterface
  2. 在您的应用配置neon文件中将默认实现替换为您自己的实现
services:
	mailDownloader: Crm\YourModule\Models\MailDownloader\YourMailDownloader

升级

支付模块提供了一种非常基本的升级处理方式。升级目前不可配置,并且与预定义的规则集一起工作。有4种升级类型可用

  • 缩短。如果用户的订阅在不久的将来不会结束,系统允许通过缩短实际订阅进行升级。缩短的天数基于当前订阅剩余天数和用户升级到的订阅类型的价格。如果缩短后的订阅会在14天内结束,则不会触发缩短。
  • 付费升级。如果缩短不可用,系统会向用户提供一个付费升级选项。支付的金额基于以下因素计算
  • 付费循环升级。当用户的当前订阅是循环的并且订阅在接下来的5天内不会结束时触发。立即收费金额基于升级后的订阅类型价格和当前用户订阅剩余的天数。下一个周期的收费将以升级后订阅的价格进行。
  • 免费循环升级。如果用户剩余的订阅时间少于5天,系统允许免费升级到更高级别的订阅。下一个周期的收费将以升级后订阅的价格进行。

订阅类型的升级选项可以在CRM管理员的订阅类型详情中配置。升级不会检查每个订阅类型的内容——因此它们不能自动确定月度网络订阅可以升级到月度网络+打印订阅。每个升级都必须手动配置,并且所有升级选项始终基于实际订阅、实际订阅类型的价格和升级后订阅类型的价格进行确定。

API文档

所有示例都使用http://crm.press作为基础域名。请在执行示例之前将主机更改为您使用的域名。

所有示例都使用XXX作为授权令牌的默认值,请将其替换为真实的令牌

  • API令牌。标准API密钥用于服务器之间的通信。它标识整个调用应用程序。它们可以在CRM管理员中生成(/api/api-tokens-admin/),并且每个API密钥都必须白名单才能访问特定的API端点。默认情况下,API密钥无权访问任何端点。
  • 用户令牌。在登录过程中为每个用户生成,令牌在系统不同部分之间通信时标识单个用户。令牌可以
    • 从通过CRM登录的用户的n_token cookie中读取
    • /api/v1/users/login端点的响应中读取——您可以将响应存储到自己的cookie/local storage/session中。

API响应可以包含以下HTTP状态码

如果可能,响应将包括application/json编码的有效负载,其中包含进一步解释错误的消息。

POST /api/v1/payments/paypal-ipn

处理来自PayPal的IPN通知。遵循此指南以启用您的PayPal账户的IPN通知。使用“通知URL”http://crm.press/api/v1/payments/paypal-ipn(将http://crm.press替换为您的域名)。

注意:切换到“实时”模式时,别忘了也将paypal_ipn_baseurl配置更改为实时IPN端点(见此处

GET /api/v1/payments/variable-symbol

API调用返回用于新支付实例的唯一变量符号(事务ID)。

头部
示例
curl -X POST \
  http://crm.press:8080/api/v1/payments/variable-symbol \
  -H 'Accept: application/json, text/plain, */*' \
  -H 'Authorization: Bearer XXX'

响应

{
    "status": "ok",
    "variable_symbol": "2735309229"
}

GET /api/v1/users/recurrent-payments

API调用返回用户所有定期支付的列表。

头部
参数
示例
curl 'http://crm.press/api/v1/users/recurrent-payments?states[]=active&states[]=user_stop&chargeable_from=2020-07-10T09%3A13%3A38%2B00%3A00' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer XXX'

响应

[
  {
    "id": 154233,
    "parent_payment_id": 1231610,
    "charge_at": "2020-10-07T08:54:00+02:00",
    "payment_gateway_code": "stripe_recurrent",
    "subscription_type_code": "sample",
    "state": "active",
    "retries": 4
  }
]

POST /api/v1/recurrent-payment/reactivate

API调用用于重新激活用户的定期支付。

成功重新激活定期支付的条件

  • RecurrentPayment必须处于\Crm\PaymentsModule\Repository\RecurrentPaymentsRepository::STATE_USER_STOP状态。
  • 下一次支付必须在将来(>=现在)。

变更

  • 定期支付的状态设置为\Crm\PaymentsModule\Repository\RecurrentPaymentsRepository::STATE_ACTIVE
头部
有效负载参数
示例
curl -X POST 'http://crm.press/api/v1/recurrent-payment/reactivate' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer XXX' \
--data-raw '{
    "id": 999999
}'

响应

成功时返回HTTP状态200 OK,并带有定期支付详情。

{
  "id": 999999,
  "parent_payment_id": 1234567,
  "charge_at": "2020-10-07T08:54:00+02:00", // charge is not changed when reactivating recurrent
  "payment_gateway_code": "stripe_recurrent",
  "subscription_type_code": "sample",
  "state": "active", // on success, state is always set to `active`
  "retries": 4
}

除了API文档部分开头描述的API响应外

POST /api/v1/recurrent-payment/stop

API调用用于停止用户的定期支付。

成功停止定期支付的条件

  • RecurrentPayment必须处于\Crm\PaymentsModule\Repository\RecurrentPaymentsRepository::STATE_ACTIVE状态。

变更

  • 定期支付的状态设置为\Crm\PaymentsModule\Repository\RecurrentPaymentsRepository::STATE_USER_STOP
头部
有效负载参数
示例
curl -X POST 'http://crm.press/api/v1/recurrent-payment/stop' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer XXX' \
--data-raw '{
    "id": 999999
}'

响应

成功时返回HTTP状态200 OK,并带有定期支付详情。

{
  "id": 999999,
  "parent_payment_id": 1234567,
  "charge_at": "2020-10-07T08:54:00+02:00", // charge is not changed when stopping recurrent
  "payment_gateway_code": "stripe_recurrent",
  "subscription_type_code": "sample",
  "state": "user_stop", // this endpoint always sets state to `user_stop`
  "retries": 4
}

除了API文档部分开头描述的API响应外

组件

ActualFreeSubscribersStatWidget

简单的管理员仪表板小部件,显示免费订阅者数量。

alt text

源代码

如何使用

ActualPaidSubscribersStatWidget

简单的管理员仪表板小部件,显示付费订阅者数量。

alt text

源代码

如何使用

ChangePaymentStatus

管理员列表/详情更改支付状态模态组件。

alt text

源代码

如何使用

DeviceUserListingWidget

管理员用户列表设备组件。

alt text

源代码

如何使用

DonationPaymentItemListWidget

alt text

源代码

如何使用

DupliciteRecurrentPayments

管理员列表重复的定期支付。

alt text

源代码

如何使用

LastPayments

管理员列表支付网关详情中的最后支付。

alt text

源代码

如何使用

MonthAmountStatWidget

管理员仪表板简单统计小部件,显示上个月的支付金额。

alt text

源代码

如何使用

MonthToDateAmountStatWidget

管理员仪表板简单统计小部件,显示上个月的支付金额。

alt text

源代码

如何使用

MyNextRecurrentPayment

alt text

源代码

如何使用

ParsedMails

Payments管理员小部件显示金额错误的支付。

alt text

源代码

如何使用

PaymentItemsListWidget

管理员列表中支付详情的支付项。

alt text

源代码

如何使用

SubscribersWithPaymentWidget

管理员仪表板单统计小部件。

alt text

源代码

如何使用

SubscriptionsWithActiveUnchargedRecurrentEndingWithinPeriodWidget

管理员列表小部件。

alt text

源代码

如何使用

SubscriptionsWithoutExtensionEndingWithinPeriodWidget

管理员仪表板统计小部件。

alt text

源代码

如何使用

SubscriptionTypeReports

管理员订阅类型详细统计小部件。

alt text

源代码

如何使用

TodayAmountStatWidget

管理员仪表板简单单统计小部件。

alt text

源代码

如何使用

TotalAmountStatWidget

管理员仪表板简单单统计小部件。

alt text

源代码

如何使用

TotalUserPayments

管理员用户详情统计小部件。

alt text

源代码

如何使用

UserPayments

管理员用户详情列表小部件。

alt text

源代码

如何使用

确认电子邮件

来自银行的确认电子邮件示例。每种类型的邮件都可以在应用程序配置中配置自己的IMAP访问。

TatraBanka Simple

发件人: b-mail (at) tatrabanka (dot) sk

主题: e-commerce

示例电子邮件文件

TatraBanka

发件人: b-mail (at) tatrabanka (dot) sk

主题: 银行账户上的信贷

示例电子邮件文件

TatraBanka 对账单

发件人: vypis_obchodnik (at) tatrabanka (dot) sk

示例电子邮件文件

CSOB

发件人: notification (at) csob (dot) cz

主题: CEB Info: 记账支付

示例电子邮件文件

斯洛伐克CSOB

发件人: AdminTBS (at) csob (dot) sk

主题: CSOB Info 24 - 通知

示例电子邮件文件