4002001, 'message' => 'beneficiary exists'], ['code' => 200, 'data' => ['beneficiary_id' => 5006]], [ 'code' => 200, 'data' => [ 'transaction_status' => 'PROCESSING', 'transaction_id' => 'T100', ], ], ]); $logic = new PayPlusCashierLogic($service); $result = $logic->createBeneficiaryAndPayout([ 'user_id' => 10001, 'amount' => 10, 'currency' => 'USD', 'order_id' => 'TX100', 'pix_type' => 1, 'cashapp_account' => 'cashuser', 'email' => 'user@example.com', ]); $this->assertSame('PROCESSING', $result['data']['transaction_status']); $this->assertSame('/rest/v2/beneficiaries/create', $service->calls[0]['path']); $this->assertSame('/rest/v2/beneficiaries/query', $service->calls[1]['path']); $this->assertSame('/rest/v2/payouts/cashApp', $service->calls[2]['path']); $this->assertSame('$cashuser', $service->calls[2]['payload']['cashapp_account']); } /** @test */ public function it_does_not_payout_when_beneficiary_id_cannot_be_resolved() { $service = new FakePayPlusForCashier([ ['code' => 4002001, 'message' => 'beneficiary exists'], ['code' => 200], ]); $logic = new PayPlusCashierLogic($service); $result = $logic->createBeneficiaryAndPayout([ 'user_id' => 10001, 'amount' => 10, 'currency' => 'USD', 'order_id' => 'TX100', 'pix_type' => 2, 'email' => 'user@example.com', ]); $this->assertFalse($result); $this->assertCount(2, $service->calls); } /** @test */ public function it_maps_payplus_payout_statuses() { $logic = new PayPlusCashierLogic(new PayPlus([])); $this->assertSame(1, $logic->resolvePayoutStatus('PAID')); $this->assertSame(2, $logic->resolvePayoutStatus('REJECTED')); $this->assertSame(2, $logic->resolvePayoutStatus('REFUNDED')); $this->assertSame(0, $logic->resolvePayoutStatus('PROCESSING')); } /** @test */ public function it_queries_payout_detail_for_notify_and_uses_query_status() { $service = new FakePayPlusForCashier([ [ 'code' => 200, 'data' => [ 'reference_id' => 'TX100', 'transaction_id' => 'T100', 'transaction_status' => 'PAID', 'description' => 'paid', ], ], ]); $logic = new PayPlusCashierLogic($service); $detail = $logic->queryPayoutDetailForNotify([ 'reference_id' => 'TX100', 'transaction_id' => 'T100', 'status' => 'REJECTED', ]); $this->assertSame('/rest/v2/payouts/detail', $service->calls[0]['path']); $this->assertSame('TX100', $service->calls[0]['payload']['reference_id']); $this->assertSame('T100', $service->calls[0]['payload']['transaction_id']); $this->assertSame('PAID', $detail['transaction_status']); $this->assertSame(1, $logic->resolvePayoutStatus($detail['transaction_status'])); } /** @test */ public function it_returns_empty_notify_detail_when_query_has_no_order_data() { $service = new FakePayPlusForCashier([ ['code' => 200], ]); $logic = new PayPlusCashierLogic($service); $detail = $logic->queryPayoutDetailForNotify([ 'reference_id' => 'TX100', 'transaction_id' => 'T100', 'status' => 'PAID', ]); $this->assertSame([], $detail); $this->assertCount(1, $service->calls); } /** @test */ public function it_keeps_processing_payout_notify_unhandled_after_query() { $service = new FakePayPlusForCashier([ [ 'code' => 200, 'data' => [ 'reference_id' => 'TX100', 'transaction_id' => 'T100', 'transaction_status' => 'PROCESSING', ], ], ]); $logic = new PayPlusCashierLogic($service); $detail = $logic->queryPayoutDetailForNotify([ 'reference_id' => 'TX100', 'transaction_id' => 'T100', 'status' => 'REJECTED', ]); $this->assertSame('PROCESSING', $detail['transaction_status']); $this->assertSame(0, $logic->resolvePayoutStatus($detail['transaction_status'])); } } class FakePayPlusForCashier extends PayPlus { public $calls = []; private $responses; public function __construct(array $responses) { parent::__construct([ 'appId' => 200000, 'appKey' => 'test-key', 'currency' => 'USD', ]); $this->responses = $responses; } public function postPayout(string $path, array $payload) { $this->calls[] = [ 'path' => $path, 'payload' => $payload, ]; return array_shift($this->responses); } }