Files
xin-procurement/tests/Feature/MiniWechatPaymentTest.php
2026-09-03 20:36:09 +08:00

420 lines
22 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace Tests\Feature;
use App\Models\BillModel;
use App\Models\NoticeModel;
use App\Models\PaymentModel;
use App\Models\PurchaseOrderModel;
use App\Models\StoreModel;
use App\Services\WechatPayService;
use EasyWeChat\Kernel\Support\AesGcm;
use Illuminate\Support\Facades\Http;
use Symfony\Component\HttpClient\MockHttpClient;
use Symfony\Component\HttpClient\Response\MockResponse;
/**
* 小程序在线支付(微信官方渠道 APIv3,EasyWeChat):下单 → 调起支付 → 后台通知/主动查询结账
*
* - Symfony MockHttpClient 模拟微信支付接口,不触网;商户私钥/证书为测试专用自签名(与生产无关)
* - 通知报文以 APIv3 密钥 AES-256-GCM 加密构造,走真实解密链路
* - 配置平台证书时走真实 Wechatpay-Signature 验签链路
* - 结账幂等:重复通知/查询不重复累加门店总采购金额
*/
class MiniWechatPaymentTest extends ProcurementTestCase
{
/** 测试专用商户 API 私钥(自签名,仅测试使用) */
private const string TEST_PRIVATE_KEY = "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQChGoUMvCaiaf97\nXcUExEA3VP4FVwUCPQf/GPvm2cjHZACcPj65rjKNs5nJ0xaKELrJkUCZ3+N5YJJ4\nVGBBhh9GXekrzFckeqUn1sRS9FFbWfNeqAvz3qgps+ohnriPMkTRTw/SONTVRuNV\nesKz8FTYbT5Vl6jSHv71CMMcm6qrhvZ9av1nB03MrUN06v2OMhfXrZTBV5KI5zvI\nOwxetOz0MTAHcfRyrW8cejiJo3baRsRRHXsnDRB7p4H1KIPx+N7yTJKv/L2+tgXY\nmhIjKJRC5hDmrLrLOzievqKlBTsBcG41UWzcLEDG96BcR6Le+dzlJIXXi2taoFXG\nFuDUVt/bAgMBAAECggEAEagPv57kt8AuQ/MO+MAZhExIWtrNWGV1Tua0v6RMIQOQ\nyDmk3kIn6ufYNF/HhJzjcTFThzRxKzMDr98aKD7u2+NxqV8yiHOjTi53mIU2YBtc\n/5l4qzhSmIojYEk+U6i1aiCOzKzLMWTHiELKvAsxrAFXvOxrkDvdbYrm92ey2Nbm\nnvFTzTlPNEVf8HwKIFpbbV/r5RlrqDOqj8np6t5X+PlpL4Kx+oO8XVaaWjjb+2NB\nPX7laZNLa2oJ+sY4qWnwPyCeZQu2glPtBjJQRWX8+eP5p7xMMWylosL056LHasb2\nNC+KDjXSA/4O19NhBwsX8Q7QDbuJrVt/2WMboJxfYQKBgQDU7U64gzdTFezTgJ18\nb4hXqvVyzksireABV2BfKiah25yMH5s4YtgFneusn0flh5yKOh1phbn9EmMGAnob\nyHYfcyiFFR1i+4j3ELblKTkbXeaMIthTqL9QRULc3HwVjKINs3tUAlpSHqdFrzAT\nQb8JcgHfmnDrqSNCNXLJY1cgLwKBgQDBsXj7PDX+URT4m7kMWD4GzTlAbAotLBO5\nGxugKAxczGbAk3G+xmYbwgbuKUHTzBAXX0atfCMjbK9DdowRlyVbI948o8drXqz2\nR2l6u3VyV0ERP+9BV0xfLNbiQGWiPZiYR9hYg+ewbzCmsQEyAUJGisxPZZtNz598\nmqhhQLeEFQKBgQCqTCJp8IiPKzn+7x8GJy4k79bfu57cXbSLXhb2BgBf9AWBDMZY\nkrWzyFp19e7K5WuOImzjuNDIV6xbYh/HmMzg2nnN9tVKFWO2NugQ4KeL+84oxrW+\nM3jP+pU/kBiuI4x46NP6nOcgRuQCF9ubizn/k+9rp1opIV3R1m24JHvKgQKBgFUd\nZtuIHvXtrXh9/bg2ArO8dR8hGuu0xcn+5onfb2dMDw8q+73oszAZeDAqAxpOPvf2\nTzmnJk3H0dCkhHKqZ6kAPwWItvYwuXLT+L8NJ1QikZ5B6SJeeVoNezQbNk4wISEx\n2Rk0hZibk9Z8S9ksgnI7RgLR1IhB54S69ake5kXtAoGANB/pN7MBP9ZeUBKq6eoB\nTHcHZjcD8ZRC/4rsgDcwpHd67xWloALc7FXA1gQfX2VcwFbtf4pl6umQd8kmsmEN\n0AJJ+riyMMrPcHZV0PCo9Kq4ex+6bLFENdRbjUBNk2nrQ/le8BxZLNi0QZPZTOLW\n+XXHpYd3dKJ/BJA/RfiP7sg=\n-----END PRIVATE KEY-----";
/** 测试专用商户 API 证书(自签名 X509,请求签名序列号取自证书) */
private const string TEST_CERTIFICATE = "-----BEGIN CERTIFICATE-----\nMIIC4TCCAcmgAwIBAgIUIDIEwVtecMJKfY0NCx1l8Fwv7LowDQYJKoZIhvcNAQEL\nBQAwADAeFw0yNjA5MDExMzM5NThaFw0zNjA4MjkxMzM5NThaMAAwggEiMA0GCSqG\nSIb3DQEBAQUAA4IBDwAwggEKAoIBAQChGoUMvCaiaf97XcUExEA3VP4FVwUCPQf/\nGPvm2cjHZACcPj65rjKNs5nJ0xaKELrJkUCZ3+N5YJJ4VGBBhh9GXekrzFckeqUn\n1sRS9FFbWfNeqAvz3qgps+ohnriPMkTRTw/SONTVRuNVesKz8FTYbT5Vl6jSHv71\nCMMcm6qrhvZ9av1nB03MrUN06v2OMhfXrZTBV5KI5zvIOwxetOz0MTAHcfRyrW8c\nejiJo3baRsRRHXsnDRB7p4H1KIPx+N7yTJKv/L2+tgXYmhIjKJRC5hDmrLrLOzie\nvqKlBTsBcG41UWzcLEDG96BcR6Le+dzlJIXXi2taoFXGFuDUVt/bAgMBAAGjUzBR\nMB0GA1UdDgQWBBQ2SD9beEd/0EU6ouWHHSW6Pdh8OjAfBgNVHSMEGDAWgBQ2SD9b\neEd/0EU6ouWHHSW6Pdh8OjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUA\nA4IBAQByZckpIfdi5BeWwX4FZK9D5+78hZpOpDi+a8rxyd22BYYbkft5WfWnTjWh\nuXZgwOBPoYwmQ9FtC8rIeXWCadcp/ohGCkVVm9Y2CtkutrTC+uxdl4LnDPRQjXPL\nuRubAzvv3leRrphd1dOmAku32TfE0bK6Xul6pCnQaEgIlFp8zp8UkCrYcm9SPNye\nRt/1vt5iBPB6UiZ9oQL1IDUdqJV2Hl9snzHi0AzuZHy+ZMZY6WEY2lBohN8wvfak\nqrejyeA3nY7+a+QRPCg9fAOhxJlIj/eJXJXUUgJTiwTDHN16JQYcqgX5/AdrqnNj\ndkhBOR8mPPmE9hCoXwcDt017y7YV\n-----END CERTIFICATE-----";
/** 测试专用 APIv3 密钥(32 字节) */
private const string TEST_SECRET_KEY = 'testSecr3tKey0123456789abcdef01';
/** 测试专用平台证书序列号 */
private const string TEST_PLATFORM_SERIAL = 'TESTPUBKEYID0001';
protected function setUp(): void
{
parent::setUp();
config([
'services.wechat.mini.appid' => 'wx-mini-test',
'services.wechat.mini.secret' => 'wx-secret-test',
'services.pay.online_channel' => 'wechat',
'services.wxpay.mch_id' => '1630000001',
'services.wxpay.private_key' => self::TEST_PRIVATE_KEY,
'services.wxpay.certificate' => self::TEST_CERTIFICATE,
'services.wxpay.secret_key' => self::TEST_SECRET_KEY,
]);
}
/**
* 以 MockHttpClient 替换微信支付接口应答,并注入容器(控制器经由容器取同一实例)
*
* @param callable|array<int, MockResponse> $responses 应答回调或应答队列
*/
private function fakeWechatApi(callable|array $responses): WechatPayService
{
$service = app(WechatPayService::class);
$app = $service->app();
$app->setHttpClient(new MockHttpClient($responses, 'https://api.mch.weixin.qq.com'));
$this->app->instance(WechatPayService::class, $service);
return $service;
}
/** 模拟微信 code2session 成功 */
private function fakeCode2session(string $openid = 'oOpenidTest001'): void
{
Http::fake([
'https://api.weixin.qq.com/*' => Http::response(['openid' => $openid, 'session_key' => 'sk'], 200),
]);
}
/** 造一张指定金额的未支付账单(总额=商品金额) */
private function makeBill(StoreModel $store, string $amount, array $attributes = []): BillModel
{
return BillModel::create(array_merge([
'bill_no' => 'ZD' . random_int(100000000000, 999999999999),
'purchase_id' => PurchaseOrderModel::factory()->create()->id,
'store_id' => $store->id,
'bill_date' => '2026-09-01',
'product_amount' => $amount,
'delivery_fee' => '0.00',
'box_num' => 0,
'tray_num' => 0,
'box_price' => '0.00',
'tray_price' => '0.00',
'added_amount' => '0.00',
'total_amount' => $amount,
'status' => BillModel::STATUS_UNPAID,
], $attributes));
}
/** 造一笔待支付的微信渠道在线支付单并锁定账单 */
private function makeWechatPayment(StoreModel $store, string $amount, BillModel ...$bills): PaymentModel
{
$payment = PaymentModel::create([
'payment_no' => 'ZF' . now()->format('Ymd') . random_int(1000, 9999),
'store_id' => $store->id,
'amount' => $amount,
'pay_type' => PaymentModel::TYPE_ONLINE,
'pay_method' => PaymentModel::METHOD_WECHAT,
'voucher_ids' => '',
'status' => PaymentModel::STATUS_PENDING,
'openid' => 'oOpenidTest001',
]);
foreach ($bills as $bill) {
$bill->update(['payment_id' => $payment->id]);
}
return $payment;
}
/**
* 构造微信支付结果通知报文(交易报文以 APIv3 密钥 AES-256-GCM 加密装入 resource
*
* @param array<string, mixed> $resourceOverrides 交易报文覆盖项
* @return array<string, mixed> 通知报文
*/
private function wechatNotifyBody(PaymentModel $payment, array $resourceOverrides = []): array
{
$resource = array_merge([
'mchid' => '1630000001',
'appid' => 'wx-mini-test',
'out_trade_no' => $payment->payment_no,
'transaction_id' => '4200002501202609010001',
'trade_state' => 'SUCCESS',
'trade_state_desc' => '支付成功',
'success_time' => '2026-09-01T14:00:00+08:00',
'amount' => [
'total' => (int) bcmul((string) $payment->amount, '100'),
'payer_total' => (int) bcmul((string) $payment->amount, '100'),
'currency' => 'CNY',
],
], $resourceOverrides);
return [
'id' => 'EV-202609011400000001',
'create_time' => '2026-09-01T14:00:05+08:00',
'resource_type' => 'encrypt-resource',
'event_type' => 'TRANSACTION.SUCCESS',
'summary' => '支付成功',
'resource' => [
'original_type' => 'transaction',
'algorithm' => 'AEAD_AES_256_GCM',
'ciphertext' => AesGcm::encrypt((string) json_encode($resource), self::TEST_SECRET_KEY, 'nonce1234567', 'transaction'),
'associated_data' => 'transaction',
'nonce' => 'nonce1234567',
],
];
}
/** 以原始报文 + 自定义请求头发送微信通知(验签场景需要精确控制 body 参与签名) */
private function postWechatNotify(array $body, array $headers = []): \Illuminate\Testing\TestResponse
{
$server = ['CONTENT_TYPE' => 'application/json'];
foreach ($headers as $name => $value) {
$server['HTTP_' . strtoupper(str_replace('-', '_', $name))] = $value;
}
return $this->call('POST', '/mini/payment/wechat-notify', [], [], [], $server, (string) json_encode($body));
}
/** 对通知报文按微信规则签名(平台证书验签场景):timestamp\nnonce\nbody\n */
private function signNotify(string $body, int $timestamp, string $nonce): string
{
openssl_sign("{$timestamp}\n{$nonce}\n{$body}\n", $signature, self::TEST_PRIVATE_KEY, 'sha256WithRSAEncryption');
return base64_encode($signature);
}
/** 发起微信渠道在线支付:锁定账单、创建支付单、上送报文正确、返回标准 wx.requestPayment 调起参数 */
public function test_create_wechat_payment_success(): void
{
$this->fakeCode2session();
$captured = [];
$this->fakeWechatApi(function (string $method, string $url, array $options) use (&$captured) {
$captured = compact('method', 'url', 'options');
return new MockResponse((string) json_encode(['prepay_id' => 'wx202609011200000001']), ['http_code' => 200]);
});
$store = StoreModel::factory()->create();
$bill1 = $this->makeBill($store, '100.00');
$bill2 = $this->makeBill($store, '50.50');
$this->actingAsMiniStore($store);
$response = $this->postJson('/mini/payment/online', [
'bill_ids' => [$bill1->id, $bill2->id],
'code' => 'wx-login-code',
])->assertJsonPath('success', true);
$paymentNo = $response->json('data.payment_no');
$this->assertSame('150.50', $response->json('data.amount'));
// 调起参数为标准 wx.requestPayment 五要素,package 含 prepay_id
$payParams = $response->json('data.pay_params');
$this->assertSame('wx-mini-test', $payParams['appId'] ?? null);
$this->assertNotEmpty($payParams['timeStamp']);
$this->assertNotEmpty($payParams['nonceStr']);
$this->assertSame('prepay_id=wx202609011200000001', $payParams['package']);
$this->assertSame('RSA', $payParams['signType']);
$this->assertNotEmpty($payParams['paySign']);
$payment = PaymentModel::where('payment_no', $paymentNo)->first();
$this->assertSame(PaymentModel::TYPE_ONLINE, $payment->pay_type);
$this->assertSame(PaymentModel::METHOD_WECHAT, $payment->pay_method);
$this->assertSame(PaymentModel::STATUS_PENDING, $payment->status);
$this->assertSame('', (string) $payment->order_id, '微信渠道下单成功后才回传 transaction_id');
// 账单锁定 + openid 绑定门店
$this->assertSame($payment->id, $bill1->fresh()->payment_id);
$this->assertSame($payment->id, $bill2->fresh()->payment_id);
$this->assertSame('oOpenidTest001', $store->fresh()->openid);
// 上送微信的报文:JSAPI 下单,金额转换为分,通知地址为微信回调路由
//MockHttpClient 回调收到的是预处理后的 optionsjson 已编码进 bodyheader 名小写且值为数组)
$this->assertSame('POST', $captured['method']);
$this->assertStringContainsString('/v3/pay/transactions/jsapi', $captured['url']);
$json = (array) json_decode((string) ($captured['options']['body'] ?? ''), true);
$this->assertSame('wx-mini-test', $json['appid']);
$this->assertSame('1630000001', $json['mchid']);
$this->assertSame($paymentNo, $json['out_trade_no']);
$this->assertSame(15050, $json['amount']['total']);
$this->assertSame('CNY', $json['amount']['currency']);
$this->assertSame('oOpenidTest001', $json['payer']['openid']);
$this->assertStringContainsString('/mini/payment/wechat-notify', $json['notify_url']);
// 请求头携带商户签名(预处理后的 headers 为 "Name: value" 字符串列表)
$headerLines = implode("\n", array_map('strval', (array) ($captured['options']['headers'] ?? [])));
$this->assertStringContainsString('Authorization: WECHATPAY2-SHA256-RSA2048', $headerLines);
}
/** 微信下单失败(应答 4xx):支付单作废并释放账单,可重新发起 */
public function test_create_wechat_payment_gateway_failure_releases_bills(): void
{
$this->fakeCode2session();
$this->fakeWechatApi([
new MockResponse((string) json_encode(['code' => 'PARAM_ERROR', 'message' => 'appid 与 mchid 不匹配']), ['http_code' => 400]),
]);
$store = StoreModel::factory()->create();
$bill = $this->makeBill($store, '20.00');
$this->actingAsMiniStore($store);
$this->postJson('/mini/payment/online', ['bill_ids' => [$bill->id], 'code' => 'c'])
->assertJsonPath('success', false);
$payment = PaymentModel::first();
$this->assertSame(PaymentModel::STATUS_REJECTED, $payment->status);
$this->assertSame(0, $bill->fresh()->payment_id, '账单释放可重新付款');
}
/** 微信支付成功通知:AES-GCM 解密 → 幂等结账(账单置已支付 + 累加门店总采购金额 + 通知门店) */
public function test_wechat_notify_settles_payment(): void
{
$store = StoreModel::factory()->create();
$bill1 = $this->makeBill($store, '100.00');
$bill2 = $this->makeBill($store, '50.00');
$payment = $this->makeWechatPayment($store, '150.00', $bill1, $bill2);
$this->postWechatNotify($this->wechatNotifyBody($payment))
->assertJsonPath('code', 'SUCCESS');
$payment->refresh();
$this->assertSame(PaymentModel::STATUS_APPROVED, $payment->status);
$this->assertSame('4200002501202609010001', $payment->trade_no);
$this->assertSame('2026-09-01 14:00:00', (string) $payment->paid_at);
foreach ([$bill1, $bill2] as $bill) {
$bill->refresh();
$this->assertSame(BillModel::STATUS_PAID, $bill->status);
$this->assertStringContainsString('微信支付', (string) $bill->pay_remark);
$this->assertStringContainsString($payment->payment_no, (string) $bill->pay_remark);
}
$this->assertSame('150.00', (string) $store->fresh()->total_purchase_amount);
$this->assertTrue(
NoticeModel::where('store_id', $store->id)->where('title', '账单支付成功')->exists()
);
// 重复通知幂等:仍应答成功,金额不重复累加
$this->postWechatNotify($this->wechatNotifyBody($payment))
->assertJsonPath('code', 'SUCCESS');
$this->assertSame('150.00', (string) $store->fresh()->total_purchase_amount);
$this->assertSame(1, NoticeModel::where('store_id', $store->id)->count());
}
/** 通知解密失败 / 金额不一致 / 订单号不存在 / 非支付成功状态 / 商户号不一致:应答失败且不结账 */
public function test_wechat_notify_rejects_invalid_messages(): void
{
$store = StoreModel::factory()->create();
$bill = $this->makeBill($store, '100.00');
$payment = $this->makeWechatPayment($store, '100.00', $bill);
// 密文非法(密钥不匹配)→ 解密失败
$badBody = $this->wechatNotifyBody($payment);
$badBody['resource']['ciphertext'] = AesGcm::encrypt('{"out_trade_no":"x"}', str_repeat('x', 32), 'nonce1234567', 'transaction');
$this->postWechatNotify($badBody)->assertJsonPath('code', 'FAIL');
// 金额不一致(防篡改)
$this->postWechatNotify($this->wechatNotifyBody($payment, ['amount' => ['total' => 9999, 'payer_total' => 9999]]))
->assertJsonPath('code', 'FAIL');
// 订单号不存在
$this->postWechatNotify($this->wechatNotifyBody($payment, ['out_trade_no' => 'ZF000000000000']))
->assertJsonPath('code', 'FAIL');
// 非支付成功状态
$this->postWechatNotify($this->wechatNotifyBody($payment, ['trade_state' => 'NOTPAY']))
->assertJsonPath('code', 'FAIL');
// 商户号不一致(防串号)
$this->postWechatNotify($this->wechatNotifyBody($payment, ['mchid' => '9999999999']))
->assertJsonPath('code', 'FAIL');
// 均未结账
$this->assertSame(PaymentModel::STATUS_PENDING, $payment->fresh()->status);
$this->assertSame(BillModel::STATUS_UNPAID, $bill->fresh()->status);
$this->assertSame('0.00', (string) $store->fresh()->total_purchase_amount);
}
/** 配置平台证书后走验签链路:签名合法才受理通知 */
public function test_wechat_notify_with_platform_cert_validates_signature(): void
{
config([
'services.wxpay.platform_cert' => self::TEST_CERTIFICATE,
'services.wxpay.platform_serial' => self::TEST_PLATFORM_SERIAL,
]);
$store = StoreModel::factory()->create();
$bill = $this->makeBill($store, '66.00');
$payment = $this->makeWechatPayment($store, '66.00', $bill);
$body = (string) json_encode($this->wechatNotifyBody($payment));
$timestamp = time();
$nonce = 'notifyNonce001';
// 无签名头 → 验签失败(微信请求必带签名头)
$this->postWechatNotify($this->wechatNotifyBody($payment))->assertJsonPath('code', 'FAIL');
// 签名头齐全且合法 → 受理并结账
$this->postWechatNotify($this->wechatNotifyBody($payment), [
'Wechatpay-Timestamp' => (string) $timestamp,
'Wechatpay-Nonce' => $nonce,
'Wechatpay-Serial' => self::TEST_PLATFORM_SERIAL,
'Wechatpay-Signature' => $this->signNotify($body, $timestamp, $nonce),
])->assertJsonPath('code', 'SUCCESS');
$this->assertSame(PaymentModel::STATUS_APPROVED, $payment->fresh()->status);
$this->assertSame(BillModel::STATUS_PAID, $bill->fresh()->status);
}
/** 主动查询:微信已支付则同步结账;未支付保持待支付;已结账后不再请求微信 */
public function test_wechat_query_syncs_status(): void
{
$store = StoreModel::factory()->create();
$bill = $this->makeBill($store, '80.00');
$payment = $this->makeWechatPayment($store, '80.00', $bill);
$queryCount = 0;
$this->fakeWechatApi(function (string $method, string $url) use (&$queryCount, $payment) {
$queryCount++;
$this->assertSame('GET', $method);
$this->assertStringContainsString('/v3/pay/transactions/out-trade-no/' . $payment->payment_no, $url);
$data = $queryCount === 1
? ['trade_state' => 'NOTPAY', 'out_trade_no' => $payment->payment_no]
: [
'trade_state' => 'SUCCESS',
'out_trade_no' => $payment->payment_no,
'transaction_id' => '4200002501202609010002',
'success_time' => '2026-09-01T15:00:00+08:00',
'amount' => ['total' => 8000, 'payer_total' => 8000],
];
return new MockResponse((string) json_encode($data), ['http_code' => 200]);
});
// 场景一:微信未支付
$this->actingAsMiniStore($store);
$this->getJson("/mini/payment/online/{$payment->payment_no}/query")
->assertJsonPath('success', true)
->assertJsonPath('data.status', PaymentModel::STATUS_PENDING);
$this->assertSame(BillModel::STATUS_UNPAID, $bill->fresh()->status);
// 场景二:微信已支付 → 查询即结账
$this->getJson("/mini/payment/online/{$payment->payment_no}/query")
->assertJsonPath('success', true)
->assertJsonPath('data.status', PaymentModel::STATUS_APPROVED)
->assertJsonPath('data.trade_no', '4200002501202609010002');
$this->assertSame(BillModel::STATUS_PAID, $bill->fresh()->status);
$this->assertSame('80.00', (string) $store->fresh()->total_purchase_amount);
// 已结账后重复查询不再请求微信(本地直接返回)
$this->getJson("/mini/payment/online/{$payment->payment_no}/query")
->assertJsonPath('success', true)
->assertJsonPath('data.status', PaymentModel::STATUS_APPROVED);
$this->assertSame(2, $queryCount, '已结账后不再请求微信');
$this->assertSame('80.00', (string) $store->fresh()->total_purchase_amount);
}
/** 渠道开关为旺铺(默认)时不受影响:仍走旺铺下单(pay_method=旺铺支付,旺铺通道报错而非微信) */
public function test_wangpu_channel_remains_default(): void
{
config(['services.pay.online_channel' => 'wangpu']);
Http::fake([
'https://api.weixin.qq.com/*' => Http::response(['openid' => 'oOpenidTest001'], 200),
]);
$store = StoreModel::factory()->create();
$bill = $this->makeBill($store, '10.00');
$this->actingAsMiniStore($store);
// 未配置旺铺密钥:旺铺通道在本地装信封即报错(证明分发到旺铺而非微信),支付单作废释放账单
$this->postJson('/mini/payment/online', ['bill_ids' => [$bill->id], 'code' => 'c'])
->assertJsonPath('success', false);
$payment = PaymentModel::first();
$this->assertSame(PaymentModel::METHOD_WANGPU, $payment->pay_method);
$this->assertSame(PaymentModel::STATUS_REJECTED, $payment->status);
$this->assertSame(0, $bill->fresh()->payment_id);
}
}