Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F120824306
PaymentsStripeTest.php
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Authored By
Unknown
Size
32 KB
Referenced Files
None
Subscribers
None
PaymentsStripeTest.php
View Options
<?php
namespace
Tests\Feature\Controller
;
use
App\Http\Controllers\API\V4\PaymentsController
;
use
App\Payment
;
use
App\Providers\PaymentProvider
;
use
App\Transaction
;
use
App\Wallet
;
use
App\WalletSetting
;
use
App\VatRate
;
use
GuzzleHttp\Psr7\Response
;
use
Illuminate\Support\Facades\Bus
;
use
Tests\TestCase
;
use
Tests\StripeMocksTrait
;
class
PaymentsStripeTest
extends
TestCase
{
use
StripeMocksTrait
;
/**
* {@inheritDoc}
*/
public
function
setUp
():
void
{
parent
::
setUp
();
if
(!
\config
(
'services.stripe.key'
))
{
$this
->
markTestSkipped
(
'No STRIPE_KEY'
);
}
// All tests in this file use Stripe
\config
([
'services.payment_provider'
=>
'stripe'
]);
\config
([
'app.vat.mode'
=>
0
]);
$this
->
deleteTestUser
(
'payment-test@'
.
\config
(
'app.domain'
));
$john
=
$this
->
getTestUser
(
'john@kolab.org'
);
$wallet
=
$john
->
wallets
()->
first
();
Wallet
::
where
(
'id'
,
$wallet
->
id
)->
update
([
'balance'
=>
0
]);
WalletSetting
::
where
(
'wallet_id'
,
$wallet
->
id
)->
delete
();
Transaction
::
where
(
'object_id'
,
$wallet
->
id
)
->
where
(
'type'
,
Transaction
::
WALLET_CREDIT
)->
delete
();
Payment
::
query
()->
delete
();
VatRate
::
query
()->
delete
();
}
/**
* {@inheritDoc}
*/
public
function
tearDown
():
void
{
if
(
\config
(
'services.stripe.key'
))
{
$this
->
deleteTestUser
(
'payment-test@'
.
\config
(
'app.domain'
));
$john
=
$this
->
getTestUser
(
'john@kolab.org'
);
$wallet
=
$john
->
wallets
()->
first
();
Wallet
::
where
(
'id'
,
$wallet
->
id
)->
update
([
'balance'
=>
0
]);
WalletSetting
::
where
(
'wallet_id'
,
$wallet
->
id
)->
delete
();
Transaction
::
where
(
'object_id'
,
$wallet
->
id
)
->
where
(
'type'
,
Transaction
::
WALLET_CREDIT
)->
delete
();
Payment
::
query
()->
delete
();
VatRate
::
query
()->
delete
();
}
parent
::
tearDown
();
}
/**
* Test creating/updating/deleting an outo-payment mandate
*
* @group stripe
*/
public
function
testMandates
():
void
{
Bus
::
fake
();
// Unauth access not allowed
$response
=
$this
->
get
(
"api/v4/payments/mandate"
);
$response
->
assertStatus
(
401
);
$response
=
$this
->
post
(
"api/v4/payments/mandate"
,
[]);
$response
->
assertStatus
(
401
);
$response
=
$this
->
post
(
"api/v4/payments/mandate/reset"
,
[]);
$response
->
assertStatus
(
401
);
$response
=
$this
->
put
(
"api/v4/payments/mandate"
,
[]);
$response
->
assertStatus
(
401
);
$response
=
$this
->
delete
(
"api/v4/payments/mandate"
);
$response
->
assertStatus
(
401
);
$user
=
$this
->
getTestUser
(
'john@kolab.org'
);
$wallet
=
$user
->
wallets
()->
first
();
// Test creating a mandate (invalid input)
$post
=
[];
$response
=
$this
->
actingAs
(
$user
)->
post
(
"api/v4/payments/mandate"
,
$post
);
$response
->
assertStatus
(
422
);
$json
=
$response
->
json
();
$this
->
assertSame
(
'error'
,
$json
[
'status'
]);
$this
->
assertCount
(
2
,
$json
[
'errors'
]);
$this
->
assertSame
(
'The amount field is required.'
,
$json
[
'errors'
][
'amount'
][
0
]);
$this
->
assertSame
(
'The balance field is required.'
,
$json
[
'errors'
][
'balance'
][
0
]);
// Test creating a mandate (invalid input)
$post
=
[
'amount'
=>
100
,
'balance'
=>
'a'
];
$response
=
$this
->
actingAs
(
$user
)->
post
(
"api/v4/payments/mandate"
,
$post
);
$response
->
assertStatus
(
422
);
$json
=
$response
->
json
();
$this
->
assertSame
(
'error'
,
$json
[
'status'
]);
$this
->
assertCount
(
1
,
$json
[
'errors'
]);
$this
->
assertSame
(
'The balance must be a number.'
,
$json
[
'errors'
][
'balance'
][
0
]);
// Test creating a mandate (invalid input)
$post
=
[
'amount'
=>
-
100
,
'balance'
=>
0
];
$response
=
$this
->
actingAs
(
$user
)->
post
(
"api/v4/payments/mandate"
,
$post
);
$response
->
assertStatus
(
422
);
$json
=
$response
->
json
();
$this
->
assertSame
(
'error'
,
$json
[
'status'
]);
$this
->
assertCount
(
1
,
$json
[
'errors'
]);
$min
=
$wallet
->
money
(
Payment
::
MIN_AMOUNT
);
$this
->
assertSame
(
"Minimum amount for a single payment is {$min}."
,
$json
[
'errors'
][
'amount'
]);
// Test creating a mandate (negative balance, amount too small)
Wallet
::
where
(
'id'
,
$wallet
->
id
)->
update
([
'balance'
=>
-
2000
]);
$post
=
[
'amount'
=>
Payment
::
MIN_AMOUNT
/
100
,
'balance'
=>
0
];
$response
=
$this
->
actingAs
(
$user
)->
post
(
"api/v4/payments/mandate"
,
$post
);
$response
->
assertStatus
(
422
);
$json
=
$response
->
json
();
$this
->
assertSame
(
'error'
,
$json
[
'status'
]);
$this
->
assertCount
(
1
,
$json
[
'errors'
]);
$this
->
assertSame
(
"The specified amount does not cover the balance on the account."
,
$json
[
'errors'
][
'amount'
]);
// Test creating a mandate (valid input)
$post
=
[
'amount'
=>
20.10
,
'balance'
=>
0
,
'methodId'
=>
PaymentProvider
::
METHOD_CREDITCARD
];
$response
=
$this
->
actingAs
(
$user
)->
post
(
"api/v4/payments/mandate"
,
$post
);
$response
->
assertStatus
(
200
);
$json
=
$response
->
json
();
$this
->
assertSame
(
'success'
,
$json
[
'status'
]);
$this
->
assertMatchesRegularExpression
(
'|^cs_test_|'
,
$json
[
'id'
]);
// Assert the proper payment amount has been used
// Stripe in 'setup' mode does not allow to set the amount
$payment
=
$wallet
->
payments
()->
first
();
$this
->
assertSame
(
0
,
$payment
->
amount
);
$this
->
assertSame
(
$user
->
tenant
->
title
.
" Auto-Payment Setup"
,
$payment
->
description
);
$this
->
assertSame
(
Payment
::
TYPE_MANDATE
,
$payment
->
type
);
// Test fetching the mandate information
$response
=
$this
->
actingAs
(
$user
)->
get
(
"api/v4/payments/mandate"
);
$response
->
assertStatus
(
200
);
$json
=
$response
->
json
();
$this
->
assertEquals
(
20.10
,
$json
[
'amount'
]);
$this
->
assertEquals
(
0
,
$json
[
'balance'
]);
$this
->
assertSame
(
false
,
$json
[
'isDisabled'
]);
// We would have to invoke a browser to accept the "first payment" to make
// the mandate validated/completed. Instead, we'll mock the mandate object.
$setupIntent
=
'{
"id": "AAA",
"object": "setup_intent",
"created": 123456789,
"payment_method": "pm_YYY",
"status": "succeeded",
"usage": "off_session",
"customer": null
}'
;
$paymentMethod
=
'{
"id": "pm_YYY",
"object": "payment_method",
"card": {
"brand": "visa",
"country": "US",
"last4": "4242"
},
"created": 123456789,
"type": "card"
}'
;
$client
=
$this
->
mockStripe
();
$client
->
addResponse
(
$setupIntent
);
$client
->
addResponse
(
$paymentMethod
);
// As we do not use checkout page, we do not receive a webworker request
// I.e. we have to fake the mandate id
$wallet
=
$user
->
wallets
()->
first
();
$wallet
->
setSetting
(
'stripe_mandate_id'
,
'AAA'
);
$wallet
->
setSetting
(
'mandate_disabled'
,
1
);
$response
=
$this
->
actingAs
(
$user
)->
get
(
"api/v4/payments/mandate"
);
$response
->
assertStatus
(
200
);
$json
=
$response
->
json
();
$this
->
assertEquals
(
20.10
,
$json
[
'amount'
]);
$this
->
assertEquals
(
0
,
$json
[
'balance'
]);
$this
->
assertEquals
(
'Visa (**** **** **** 4242)'
,
$json
[
'method'
]);
$this
->
assertSame
(
false
,
$json
[
'isPending'
]);
$this
->
assertSame
(
true
,
$json
[
'isValid'
]);
$this
->
assertSame
(
true
,
$json
[
'isDisabled'
]);
// Test updating mandate details (invalid input)
$wallet
->
setSetting
(
'mandate_disabled'
,
null
);
$wallet
->
balance
=
1000
;
$wallet
->
save
();
$user
->
refresh
();
$post
=
[];
$response
=
$this
->
actingAs
(
$user
)->
put
(
"api/v4/payments/mandate"
,
$post
);
$response
->
assertStatus
(
422
);
$json
=
$response
->
json
();
$this
->
assertSame
(
'error'
,
$json
[
'status'
]);
$this
->
assertCount
(
2
,
$json
[
'errors'
]);
$this
->
assertSame
(
'The amount field is required.'
,
$json
[
'errors'
][
'amount'
][
0
]);
$this
->
assertSame
(
'The balance field is required.'
,
$json
[
'errors'
][
'balance'
][
0
]);
$post
=
[
'amount'
=>
-
100
,
'balance'
=>
0
];
$response
=
$this
->
actingAs
(
$user
)->
put
(
"api/v4/payments/mandate"
,
$post
);
$response
->
assertStatus
(
422
);
$json
=
$response
->
json
();
$this
->
assertSame
(
'error'
,
$json
[
'status'
]);
$this
->
assertCount
(
1
,
$json
[
'errors'
]);
$this
->
assertSame
(
"Minimum amount for a single payment is {$min}."
,
$json
[
'errors'
][
'amount'
]);
// Test updating a mandate (valid input)
$client
->
addResponse
(
$setupIntent
);
$client
->
addResponse
(
$paymentMethod
);
$post
=
[
'amount'
=>
30.10
,
'balance'
=>
10
];
$response
=
$this
->
actingAs
(
$user
)->
put
(
"api/v4/payments/mandate"
,
$post
);
$response
->
assertStatus
(
200
);
$json
=
$response
->
json
();
$this
->
assertSame
(
'success'
,
$json
[
'status'
]);
$this
->
assertSame
(
'The auto-payment has been updated.'
,
$json
[
'message'
]);
$this
->
assertEquals
(
30.10
,
$wallet
->
getSetting
(
'mandate_amount'
));
$this
->
assertEquals
(
10
,
$wallet
->
getSetting
(
'mandate_balance'
));
$this
->
assertSame
(
'AAA'
,
$json
[
'id'
]);
$this
->
assertFalse
(
$json
[
'isDisabled'
]);
// Test updating a disabled mandate (invalid input)
$wallet
->
setSetting
(
'mandate_disabled'
,
1
);
$wallet
->
balance
=
-
2000
;
$wallet
->
save
();
$user
->
refresh
();
// required so the controller sees the wallet update from above
$post
=
[
'amount'
=>
15.10
,
'balance'
=>
1
];
$response
=
$this
->
actingAs
(
$user
)->
put
(
"api/v4/payments/mandate"
,
$post
);
$response
->
assertStatus
(
422
);
$json
=
$response
->
json
();
$this
->
assertSame
(
'error'
,
$json
[
'status'
]);
$this
->
assertCount
(
1
,
$json
[
'errors'
]);
$this
->
assertSame
(
'The specified amount does not cover the balance on the account.'
,
$json
[
'errors'
][
'amount'
]);
// Test updating a disabled mandate (valid input)
$client
->
addResponse
(
$setupIntent
);
$client
->
addResponse
(
$paymentMethod
);
$post
=
[
'amount'
=>
30
,
'balance'
=>
1
];
$response
=
$this
->
actingAs
(
$user
)->
put
(
"api/v4/payments/mandate"
,
$post
);
$response
->
assertStatus
(
200
);
$json
=
$response
->
json
();
$this
->
assertSame
(
'success'
,
$json
[
'status'
]);
$this
->
assertSame
(
'The auto-payment has been updated.'
,
$json
[
'message'
]);
$this
->
assertSame
(
'AAA'
,
$json
[
'id'
]);
$this
->
assertFalse
(
$json
[
'isDisabled'
]);
Bus
::
assertDispatchedTimes
(
\App\Jobs\WalletCharge
::
class
,
1
);
Bus
::
assertDispatched
(
\App\Jobs\WalletCharge
::
class
,
function
(
$job
)
use
(
$wallet
)
{
$job_wallet_id
=
$this
->
getObjectProperty
(
$job
,
'walletId'
);
return
$job_wallet_id
===
$wallet
->
id
;
});
$this
->
unmockStripe
();
// Test mandate reset
$wallet
->
payments
()->
delete
();
$response
=
$this
->
actingAs
(
$user
)->
post
(
"api/v4/payments/mandate/reset"
,
[]);
$response
->
assertStatus
(
200
);
$payment
=
$wallet
->
payments
()->
first
();
$this
->
assertSame
(
0
,
$payment
->
amount
);
$this
->
assertSame
(
$user
->
tenant
->
title
.
" Auto-Payment Setup"
,
$payment
->
description
);
$this
->
assertSame
(
Payment
::
TYPE_MANDATE
,
$payment
->
type
);
// Delete mandate
$wallet
->
setSetting
(
'mandate_disabled'
,
1
);
$client
=
$this
->
mockStripe
();
$client
->
addResponse
(
$setupIntent
);
$client
->
addResponse
(
$paymentMethod
);
$client
->
addResponse
(
$paymentMethod
);
$response
=
$this
->
actingAs
(
$user
)->
delete
(
"api/v4/payments/mandate"
);
$response
->
assertStatus
(
200
);
$this
->
assertNull
(
$wallet
->
getSetting
(
'mandate_disabled'
));
$this
->
assertNull
(
$wallet
->
getSetting
(
'stripe_mandate_id'
));
$this
->
unmockStripe
();
}
/**
* Test creating a payment and receiving a status via webhook
*
* @group stripe
*/
public
function
testStoreAndWebhook
():
void
{
Bus
::
fake
();
// Unauth access not allowed
$response
=
$this
->
post
(
"api/v4/payments"
,
[]);
$response
->
assertStatus
(
401
);
$user
=
$this
->
getTestUser
(
'john@kolab.org'
);
$wallet
=
$user
->
wallets
()->
first
();
$post
=
[
'amount'
=>
-
1
];
$response
=
$this
->
actingAs
(
$user
)->
post
(
"api/v4/payments"
,
$post
);
$response
->
assertStatus
(
422
);
$json
=
$response
->
json
();
$this
->
assertSame
(
'error'
,
$json
[
'status'
]);
$this
->
assertCount
(
1
,
$json
[
'errors'
]);
$min
=
$wallet
->
money
(
Payment
::
MIN_AMOUNT
);
$this
->
assertSame
(
"Minimum amount for a single payment is {$min}."
,
$json
[
'errors'
][
'amount'
]);
// Invalid currency
$post
=
[
'amount'
=>
'12.34'
,
'currency'
=>
'FOO'
,
'methodId'
=>
'creditcard'
];
$response
=
$this
->
actingAs
(
$user
)->
post
(
"api/v4/payments"
,
$post
);
$response
->
assertStatus
(
500
);
// Successful payment
$post
=
[
'amount'
=>
'12.34'
,
'currency'
=>
'CHF'
,
'methodId'
=>
'creditcard'
];
$response
=
$this
->
actingAs
(
$user
)->
post
(
"api/v4/payments"
,
$post
);
$response
->
assertStatus
(
200
);
$json
=
$response
->
json
();
$this
->
assertSame
(
'success'
,
$json
[
'status'
]);
$this
->
assertMatchesRegularExpression
(
'|^cs_test_|'
,
$json
[
'id'
]);
$payments
=
Payment
::
where
(
'wallet_id'
,
$wallet
->
id
)->
get
();
$this
->
assertCount
(
1
,
$payments
);
$payment
=
$payments
[
0
];
$this
->
assertSame
(
1234
,
$payment
->
amount
);
$this
->
assertSame
(
$user
->
tenant
->
title
.
' Payment'
,
$payment
->
description
);
$this
->
assertSame
(
'open'
,
$payment
->
status
);
$this
->
assertEquals
(
0
,
$wallet
->
balance
);
// Test the webhook
$post
=
[
'id'
=>
"evt_1GlZ814fj3SIEU8wtxMZ4Nsa"
,
'object'
=>
"event"
,
'api_version'
=>
"2020-03-02"
,
'created'
=>
1590147209
,
'data'
=>
[
'object'
=>
[
'id'
=>
$payment
->
id
,
'object'
=>
"payment_intent"
,
'amount'
=>
1234
,
'amount_capturable'
=>
0
,
'amount_received'
=>
1234
,
'capture_method'
=>
"automatic"
,
'client_secret'
=>
"pi_1GlZ7w4fj3SIEU8w1RlBpN4l_secret_UYRNDTUUU7nkYHpOLZMb3uf48"
,
'confirmation_method'
=>
"automatic"
,
'created'
=>
1590147204
,
'currency'
=>
"chf"
,
'customer'
=>
"cus_HKDZ53OsKdlM83"
,
'last_payment_error'
=>
null
,
'livemode'
=>
false
,
'metadata'
=>
[],
'receipt_email'
=>
"payment-test@kolabnow.com"
,
'status'
=>
"succeeded"
]
],
'type'
=>
"payment_intent.succeeded"
];
// Test payment succeeded event
$response
=
$this
->
webhookRequest
(
$post
);
$response
->
assertStatus
(
200
);
$this
->
assertSame
(
Payment
::
STATUS_PAID
,
$payment
->
fresh
()->
status
);
$this
->
assertEquals
(
1234
,
$wallet
->
fresh
()->
balance
);
$transaction
=
$wallet
->
transactions
()
->
where
(
'type'
,
Transaction
::
WALLET_CREDIT
)->
get
()->
last
();
$this
->
assertSame
(
1234
,
$transaction
->
amount
);
$this
->
assertSame
(
"Payment transaction {$payment->id} using Stripe"
,
$transaction
->
description
);
// Assert that email notification job wasn't dispatched,
// it is expected only for recurring payments
Bus
::
assertDispatchedTimes
(
\App\Jobs\PaymentEmail
::
class
,
0
);
// Test that balance didn't change if the same event is posted
$response
=
$this
->
webhookRequest
(
$post
);
$response
->
assertStatus
(
200
);
$this
->
assertSame
(
Payment
::
STATUS_PAID
,
$payment
->
fresh
()->
status
);
$this
->
assertEquals
(
1234
,
$wallet
->
fresh
()->
balance
);
// Test for payment failure ('failed' status)
$payment
->
refresh
();
$payment
->
status
=
Payment
::
STATUS_OPEN
;
$payment
->
save
();
$post
[
'type'
]
=
"payment_intent.payment_failed"
;
$post
[
'data'
][
'object'
][
'status'
]
=
'failed'
;
$response
=
$this
->
webhookRequest
(
$post
);
$response
->
assertStatus
(
200
);
$this
->
assertSame
(
Payment
::
STATUS_FAILED
,
$payment
->
fresh
()->
status
);
$this
->
assertEquals
(
1234
,
$wallet
->
fresh
()->
balance
);
// Assert that email notification job wasn't dispatched,
// it is expected only for recurring payments
Bus
::
assertDispatchedTimes
(
\App\Jobs\PaymentEmail
::
class
,
0
);
// Test for payment failure ('canceled' status)
$payment
->
refresh
();
$payment
->
status
=
Payment
::
STATUS_OPEN
;
$payment
->
save
();
$post
[
'type'
]
=
"payment_intent.canceled"
;
$post
[
'data'
][
'object'
][
'status'
]
=
'canceled'
;
$response
=
$this
->
webhookRequest
(
$post
);
$response
->
assertStatus
(
200
);
$this
->
assertSame
(
Payment
::
STATUS_CANCELED
,
$payment
->
fresh
()->
status
);
$this
->
assertEquals
(
1234
,
$wallet
->
fresh
()->
balance
);
// Assert that email notification job wasn't dispatched,
// it is expected only for recurring payments
Bus
::
assertDispatchedTimes
(
\App\Jobs\PaymentEmail
::
class
,
0
);
}
/**
* Test receiving webhook request for setup intent
*
* @group stripe
*/
public
function
testCreateMandateAndWebhook
():
void
{
$user
=
$this
->
getTestUser
(
'john@kolab.org'
);
$wallet
=
$user
->
wallets
()->
first
();
Wallet
::
where
(
'id'
,
$wallet
->
id
)->
update
([
'balance'
=>
-
1000
]);
// Test creating a mandate (valid input)
$post
=
[
'amount'
=>
20.10
,
'balance'
=>
0
];
$response
=
$this
->
actingAs
(
$user
)->
post
(
"api/v4/payments/mandate"
,
$post
);
$response
->
assertStatus
(
200
);
$payment
=
$wallet
->
payments
()->
first
();
$this
->
assertSame
(
Payment
::
STATUS_OPEN
,
$payment
->
status
);
$this
->
assertSame
(
Payment
::
TYPE_MANDATE
,
$payment
->
type
);
$this
->
assertSame
(
0
,
$payment
->
amount
);
$post
=
[
'id'
=>
"evt_1GlZ814fj3SIEU8wtxMZ4Nsa"
,
'object'
=>
"event"
,
'api_version'
=>
"2020-03-02"
,
'created'
=>
1590147209
,
'data'
=>
[
'object'
=>
[
'id'
=>
$payment
->
id
,
'object'
=>
"setup_intent"
,
'client_secret'
=>
"pi_1GlZ7w4fj3SIEU8w1RlBpN4l_secret_UYRNDTUUU7nkYHpOLZMb3uf48"
,
'created'
=>
1590147204
,
'customer'
=>
"cus_HKDZ53OsKdlM83"
,
'last_setup_error'
=>
null
,
'metadata'
=>
[],
'status'
=>
"succeeded"
]
],
'type'
=>
"setup_intent.succeeded"
];
Bus
::
fake
();
// Test payment succeeded event
$response
=
$this
->
webhookRequest
(
$post
);
$response
->
assertStatus
(
200
);
$payment
->
refresh
();
$this
->
assertSame
(
Payment
::
STATUS_PAID
,
$payment
->
status
);
$this
->
assertSame
(
$payment
->
id
,
$wallet
->
fresh
()->
getSetting
(
'stripe_mandate_id'
));
// Expect a WalletCharge job if the balance is negative
Bus
::
assertDispatchedTimes
(
\App\Jobs\WalletCharge
::
class
,
1
);
Bus
::
assertDispatched
(
\App\Jobs\WalletCharge
::
class
,
function
(
$job
)
use
(
$wallet
)
{
$job_wallet_id
=
TestCase
::
getObjectProperty
(
$job
,
'walletId'
);
return
$job_wallet_id
===
$wallet
->
id
;
});
// TODO: test other setup_intent.* events
}
/**
* Test automatic payment charges
*
* @group stripe
*/
public
function
testTopUpAndWebhook
():
void
{
Bus
::
fake
();
$user
=
$this
->
getTestUser
(
'john@kolab.org'
);
$wallet
=
$user
->
wallets
()->
first
();
// Stripe API does not allow us to create a mandate easily
// That's why we we'll mock API responses
// Create a fake mandate
$wallet
->
setSettings
([
'mandate_amount'
=>
20.10
,
'mandate_balance'
=>
10
,
'stripe_mandate_id'
=>
'AAA'
,
]);
$setupIntent
=
json_encode
([
"id"
=>
"AAA"
,
"object"
=>
"setup_intent"
,
"created"
=>
123456789
,
"payment_method"
=>
"pm_YYY"
,
"status"
=>
"succeeded"
,
"usage"
=>
"off_session"
,
"customer"
=>
null
]);
$paymentMethod
=
json_encode
([
"id"
=>
"pm_YYY"
,
"object"
=>
"payment_method"
,
"card"
=>
[
"brand"
=>
"visa"
,
"country"
=>
"US"
,
"last4"
=>
"4242"
],
"created"
=>
123456789
,
"type"
=>
"card"
]);
$paymentIntent
=
json_encode
([
"id"
=>
"pi_XX"
,
"object"
=>
"payment_intent"
,
"created"
=>
123456789
,
"amount"
=>
2010
,
"currency"
=>
"chf"
,
"description"
=>
$user
->
tenant
->
title
.
" Recurring Payment"
]);
$client
=
$this
->
mockStripe
();
$client
->
addResponse
(
$setupIntent
);
$client
->
addResponse
(
$paymentMethod
);
$client
->
addResponse
(
$setupIntent
);
$client
->
addResponse
(
$paymentIntent
);
$client
->
addResponse
(
$setupIntent
);
$client
->
addResponse
(
$paymentMethod
);
// Expect a recurring payment as we have a valid mandate at this point
$result
=
PaymentsController
::
topUpWallet
(
$wallet
);
$this
->
assertTrue
(
$result
);
// Check that the payments table contains a new record with proper amount
// There should be two records, one for the first payment and another for
// the recurring payment
$this
->
assertCount
(
1
,
$wallet
->
payments
()->
get
());
$payment
=
$wallet
->
payments
()->
first
();
$this
->
assertSame
(
2010
,
$payment
->
amount
);
$this
->
assertSame
(
$user
->
tenant
->
title
.
" Recurring Payment"
,
$payment
->
description
);
$this
->
assertSame
(
"pi_XX"
,
$payment
->
id
);
// Expect no payment if the mandate is disabled
$wallet
->
setSetting
(
'mandate_disabled'
,
1
);
$result
=
PaymentsController
::
topUpWallet
(
$wallet
);
$this
->
assertFalse
(
$result
);
$this
->
assertCount
(
1
,
$wallet
->
payments
()->
get
());
// Expect no payment if balance is ok
$wallet
->
setSetting
(
'mandate_disabled'
,
null
);
$wallet
->
balance
=
1000
;
$wallet
->
save
();
$result
=
PaymentsController
::
topUpWallet
(
$wallet
);
$this
->
assertFalse
(
$result
);
$this
->
assertCount
(
1
,
$wallet
->
payments
()->
get
());
// Expect no payment if the top-up amount is not enough
$wallet
->
setSetting
(
'mandate_disabled'
,
null
);
$wallet
->
balance
=
-
2050
;
$wallet
->
save
();
$result
=
PaymentsController
::
topUpWallet
(
$wallet
);
$this
->
assertFalse
(
$result
);
$this
->
assertCount
(
1
,
$wallet
->
payments
()->
get
());
Bus
::
assertDispatchedTimes
(
\App\Jobs\PaymentMandateDisabledEmail
::
class
,
1
);
Bus
::
assertDispatched
(
\App\Jobs\PaymentMandateDisabledEmail
::
class
,
function
(
$job
)
use
(
$wallet
)
{
$job_wallet
=
$this
->
getObjectProperty
(
$job
,
'wallet'
);
return
$job_wallet
->
id
===
$wallet
->
id
;
});
// Expect no payment if there's no mandate
$wallet
->
setSetting
(
'mollie_mandate_id'
,
null
);
$wallet
->
balance
=
0
;
$wallet
->
save
();
$result
=
PaymentsController
::
topUpWallet
(
$wallet
);
$this
->
assertFalse
(
$result
);
$this
->
assertCount
(
1
,
$wallet
->
payments
()->
get
());
Bus
::
assertDispatchedTimes
(
\App\Jobs\PaymentMandateDisabledEmail
::
class
,
1
);
$this
->
unmockStripe
();
// Test webhook
$post
=
[
'id'
=>
"evt_1GlZ814fj3SIEU8wtxMZ4Nsa"
,
'object'
=>
"event"
,
'api_version'
=>
"2020-03-02"
,
'created'
=>
1590147209
,
'data'
=>
[
'object'
=>
[
'id'
=>
$payment
->
id
,
'object'
=>
"payment_intent"
,
'amount'
=>
2010
,
'capture_method'
=>
"automatic"
,
'created'
=>
1590147204
,
'currency'
=>
"chf"
,
'customer'
=>
"cus_HKDZ53OsKdlM83"
,
'last_payment_error'
=>
null
,
'metadata'
=>
[],
'receipt_email'
=>
"payment-test@kolabnow.com"
,
'status'
=>
"succeeded"
]
],
'type'
=>
"payment_intent.succeeded"
];
// Test payment succeeded event
$response
=
$this
->
webhookRequest
(
$post
);
$response
->
assertStatus
(
200
);
$this
->
assertSame
(
Payment
::
STATUS_PAID
,
$payment
->
fresh
()->
status
);
$this
->
assertEquals
(
2010
,
$wallet
->
fresh
()->
balance
);
$transaction
=
$wallet
->
transactions
()
->
where
(
'type'
,
Transaction
::
WALLET_CREDIT
)->
get
()->
last
();
$this
->
assertSame
(
2010
,
$transaction
->
amount
);
$this
->
assertSame
(
"Auto-payment transaction {$payment->id} using Stripe"
,
$transaction
->
description
);
// Assert that email notification job has been dispatched
Bus
::
assertDispatchedTimes
(
\App\Jobs\PaymentEmail
::
class
,
1
);
Bus
::
assertDispatched
(
\App\Jobs\PaymentEmail
::
class
,
function
(
$job
)
use
(
$payment
)
{
$job_payment
=
$this
->
getObjectProperty
(
$job
,
'payment'
);
return
$job_payment
->
id
===
$payment
->
id
;
});
Bus
::
fake
();
// Test for payment failure ('failed' status)
$payment
->
refresh
();
$payment
->
status
=
Payment
::
STATUS_OPEN
;
$payment
->
save
();
$wallet
->
setSetting
(
'mandate_disabled'
,
null
);
$post
[
'type'
]
=
"payment_intent.payment_failed"
;
$post
[
'data'
][
'object'
][
'status'
]
=
'failed'
;
$response
=
$this
->
webhookRequest
(
$post
);
$response
->
assertStatus
(
200
);
$wallet
->
refresh
();
$this
->
assertSame
(
Payment
::
STATUS_FAILED
,
$payment
->
fresh
()->
status
);
$this
->
assertEquals
(
2010
,
$wallet
->
balance
);
$this
->
assertTrue
(!
empty
(
$wallet
->
getSetting
(
'mandate_disabled'
)));
// Assert that email notification job has been dispatched
Bus
::
assertDispatchedTimes
(
\App\Jobs\PaymentEmail
::
class
,
1
);
Bus
::
assertDispatched
(
\App\Jobs\PaymentEmail
::
class
,
function
(
$job
)
use
(
$payment
)
{
$job_payment
=
$this
->
getObjectProperty
(
$job
,
'payment'
);
return
$job_payment
->
id
===
$payment
->
id
;
});
Bus
::
fake
();
// Test for payment failure ('canceled' status)
$payment
->
refresh
();
$payment
->
status
=
Payment
::
STATUS_OPEN
;
$payment
->
save
();
$post
[
'type'
]
=
"payment_intent.canceled"
;
$post
[
'data'
][
'object'
][
'status'
]
=
'canceled'
;
$response
=
$this
->
webhookRequest
(
$post
);
$response
->
assertStatus
(
200
);
$this
->
assertSame
(
Payment
::
STATUS_CANCELED
,
$payment
->
fresh
()->
status
);
$this
->
assertEquals
(
2010
,
$wallet
->
fresh
()->
balance
);
// Assert that email notification job wasn't dispatched,
// it is expected only for recurring payments
Bus
::
assertDispatchedTimes
(
\App\Jobs\PaymentEmail
::
class
,
0
);
}
/**
* Test payment/top-up with VAT_MODE=1
*
* @group stripe
*/
public
function
testPaymentsWithVatModeOne
():
void
{
\config
([
'app.vat.mode'
=>
1
]);
$user
=
$this
->
getTestUser
(
'payment-test@'
.
\config
(
'app.domain'
));
$user
->
setSetting
(
'country'
,
'US'
);
$wallet
=
$user
->
wallets
()->
first
();
$vatRate
=
VatRate
::
create
([
'country'
=>
'US'
,
'rate'
=>
5.0
,
'start'
=>
now
()->
subDay
(),
]);
// Payment
$post
=
[
'amount'
=>
'10'
,
'currency'
=>
'CHF'
,
'methodId'
=>
'creditcard'
];
$response
=
$this
->
actingAs
(
$user
)->
post
(
"api/v4/payments"
,
$post
);
$response
->
assertStatus
(
200
);
// Check that the payments table contains a new record with proper amount(s)
$payment
=
$wallet
->
payments
()->
first
();
$this
->
assertSame
(
1000
+
intval
(
round
(
1000
*
$vatRate
->
rate
/
100
)),
$payment
->
amount
);
$this
->
assertSame
(
1000
,
$payment
->
credit_amount
);
$this
->
assertSame
(
$payment
->
amount
,
$payment
->
currency_amount
);
$this
->
assertSame
(
'CHF'
,
$payment
->
currency
);
$this
->
assertSame
(
$vatRate
->
id
,
$payment
->
vat_rate_id
);
$this
->
assertSame
(
'open'
,
$payment
->
status
);
$wallet
->
payments
()->
delete
();
$wallet
->
balance
=
-
1000
;
$wallet
->
save
();
// Top-up (mandate creation)
// Create a valid mandate first (expect an extra payment)
$post
=
[
'amount'
=>
20.10
,
'balance'
=>
0
,
'methodId'
=>
PaymentProvider
::
METHOD_CREDITCARD
];
$response
=
$this
->
actingAs
(
$user
)->
post
(
"api/v4/payments/mandate"
,
$post
);
$response
->
assertStatus
(
200
);
// Check that the payments table contains a new record with proper amount(s)
// Stripe mandates always use amount=0
$payment
=
$wallet
->
payments
()->
first
();
$this
->
assertSame
(
0
,
$payment
->
amount
);
$this
->
assertSame
(
0
,
$payment
->
credit_amount
);
$this
->
assertSame
(
0
,
$payment
->
currency_amount
);
$this
->
assertSame
(
null
,
$payment
->
vat_rate_id
);
$wallet
->
payments
()->
delete
();
$wallet
->
balance
=
-
1000
;
$wallet
->
save
();
// Top-up (recurring payment)
// Expect a recurring payment as we have a valid mandate at this point
// and the balance is below the threshold
$wallet
->
setSettings
([
'stripe_mandate_id'
=>
'AAA'
]);
$setupIntent
=
json_encode
([
"id"
=>
"AAA"
,
"object"
=>
"setup_intent"
,
"created"
=>
123456789
,
"payment_method"
=>
"pm_YYY"
,
"status"
=>
"succeeded"
,
"usage"
=>
"off_session"
,
"customer"
=>
null
]);
$paymentMethod
=
json_encode
([
"id"
=>
"pm_YYY"
,
"object"
=>
"payment_method"
,
"card"
=>
[
"brand"
=>
"visa"
,
"country"
=>
"US"
,
"last4"
=>
"4242"
],
"created"
=>
123456789
,
"type"
=>
"card"
]);
$paymentIntent
=
json_encode
([
"id"
=>
"pi_XX"
,
"object"
=>
"payment_intent"
,
"created"
=>
123456789
,
"amount"
=>
2010
+
intval
(
round
(
2010
*
$vatRate
->
rate
/
100
)),
"currency"
=>
"chf"
,
"description"
=>
"Recurring Payment"
]);
$client
=
$this
->
mockStripe
();
$client
->
addResponse
(
$setupIntent
);
$client
->
addResponse
(
$paymentMethod
);
$client
->
addResponse
(
$setupIntent
);
$client
->
addResponse
(
$paymentIntent
);
$result
=
PaymentsController
::
topUpWallet
(
$wallet
);
$this
->
assertTrue
(
$result
);
// Check that the payments table contains a new record with proper amount(s)
$payment
=
$wallet
->
payments
()->
first
();
$this
->
assertSame
(
2010
+
intval
(
round
(
2010
*
$vatRate
->
rate
/
100
)),
$payment
->
amount
);
$this
->
assertSame
(
2010
,
$payment
->
credit_amount
);
$this
->
assertSame
(
$payment
->
amount
,
$payment
->
currency_amount
);
$this
->
assertSame
(
$vatRate
->
id
,
$payment
->
vat_rate_id
);
}
/**
* Test listing payment methods
*
* @group stripe
*/
public
function
testListingPaymentMethods
():
void
{
Bus
::
fake
();
$user
=
$this
->
getTestUser
(
'john@kolab.org'
);
$response
=
$this
->
actingAs
(
$user
)->
get
(
'api/v4/payments/methods?type='
.
Payment
::
TYPE_ONEOFF
);
$response
->
assertStatus
(
200
);
$json
=
$response
->
json
();
$hasCoinbase
=
!
empty
(
\config
(
'services.coinbase.key'
));
$this
->
assertCount
(
2
+
intval
(
$hasCoinbase
),
$json
);
$this
->
assertSame
(
'creditcard'
,
$json
[
0
][
'id'
]);
$this
->
assertSame
(
'paypal'
,
$json
[
1
][
'id'
]);
$this
->
assertSame
(
'bitcoin'
,
$json
[
2
][
'id'
]);
$response
=
$this
->
actingAs
(
$user
)->
get
(
'api/v4/payments/methods?type='
.
Payment
::
TYPE_RECURRING
);
$response
->
assertStatus
(
200
);
$json
=
$response
->
json
();
$this
->
assertCount
(
1
,
$json
);
$this
->
assertSame
(
'creditcard'
,
$json
[
0
][
'id'
]);
}
/**
* Generate Stripe-Signature header for a webhook payload
*/
protected
function
webhookRequest
(
$post
)
{
$secret
=
\config
(
'services.stripe.webhook_secret'
);
$ts
=
time
();
$payload
=
"$ts."
.
json_encode
(
$post
);
$sig
=
sprintf
(
't=%d,v1=%s'
,
$ts
,
\hash_hmac
(
'sha256'
,
$payload
,
$secret
));
return
$this
->
withHeaders
([
'Stripe-Signature'
=>
$sig
])
->
json
(
'POST'
,
"api/webhooks/payment/stripe"
,
$post
);
}
}
File Metadata
Details
Attached
Mime Type
text/x-php
Expires
Fri, Apr 24, 10:20 AM (8 h, 18 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18866489
Default Alt Text
PaymentsStripeTest.php (32 KB)
Attached To
Mode
rK kolab
Attached
Detach File
Event Timeline