Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F117744410
D1762.1775169950.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Authored By
Unknown
Size
25 KB
Referenced Files
None
Subscribers
None
D1762.1775169950.diff
View Options
diff --git a/src/app/Documents/Receipt.php b/src/app/Documents/Receipt.php
--- a/src/app/Documents/Receipt.php
+++ b/src/app/Documents/Receipt.php
@@ -160,7 +160,7 @@
->where('status', PaymentProvider::STATUS_PAID)
->where('updated_at', '>=', $start)
->where('updated_at', '<', $end)
- ->where('amount', '>', 0)
+ ->where('amount', '<>', 0)
->orderBy('updated_at')
->get();
}
@@ -185,9 +185,17 @@
$total += $amount;
+ if ($item->type == PaymentProvider::TYPE_REFUND) {
+ $description = \trans('documents.receipt-refund');
+ } elseif ($item->type == PaymentProvider::TYPE_CHARGEBACK) {
+ $description = \trans('documents.receipt-chargeback');
+ } else {
+ $description = \trans('documents.receipt-item-desc', ['site' => $appName]);
+ }
+
return [
'amount' => $this->wallet->money($amount),
- 'description' => \trans('documents.receipt-item-desc', ['site' => $appName]),
+ 'description' => $description,
'date' => $item->updated_at->toDateString(),
];
});
diff --git a/src/app/Http/Controllers/API/V4/WalletsController.php b/src/app/Http/Controllers/API/V4/WalletsController.php
--- a/src/app/Http/Controllers/API/V4/WalletsController.php
+++ b/src/app/Http/Controllers/API/V4/WalletsController.php
@@ -179,7 +179,7 @@
$result = $wallet->payments()
->selectRaw('distinct date_format(updated_at, "%Y-%m") as ident')
->where('status', PaymentProvider::STATUS_PAID)
- ->where('amount', '>', 0)
+ ->where('amount', '<>', 0)
->orderBy('ident', 'desc')
->get()
->whereNotIn('ident', [date('Y-m')]) // exclude current month
diff --git a/src/app/Providers/Payment/Mollie.php b/src/app/Providers/Payment/Mollie.php
--- a/src/app/Providers/Payment/Mollie.php
+++ b/src/app/Providers/Payment/Mollie.php
@@ -293,6 +293,7 @@
}
// Get the payment details from Mollie
+ // TODO: Consider https://github.com/mollie/mollie-api-php/issues/502 when it's fixed
$mollie_payment = mollie()->payments()->get($payment_id);
if (empty($mollie_payment)) {
@@ -300,22 +301,44 @@
return 200;
}
+ $refunds = [];
+
if ($mollie_payment->isPaid()) {
- if (!$mollie_payment->hasRefunds() && !$mollie_payment->hasChargebacks()) {
- // The payment is paid and isn't refunded or charged back.
- // Update the balance, if it wasn't already
- if ($payment->status != self::STATUS_PAID && $payment->amount > 0) {
- $credit = true;
- $notify = $payment->type == self::TYPE_RECURRING;
+ // The payment is paid. Update the balance, and notify the user
+ if ($payment->status != self::STATUS_PAID && $payment->amount > 0) {
+ $credit = true;
+ $notify = $payment->type == self::TYPE_RECURRING;
+ }
+
+ // The payment has been (partially) refunded.
+ // Let's process refunds with status "refunded".
+ if ($mollie_payment->hasRefunds()) {
+ foreach ($mollie_payment->refunds() as $refund) {
+ if ($refund->isTransferred() && $refund->amount->value) {
+ $refunds[] = [
+ 'id' => $refund->id,
+ 'description' => $refund->description,
+ 'amount' => round(floatval($refund->amount->value) * 100),
+ 'type' => self::TYPE_REFUND,
+ // Note: we assume this is the original payment/wallet currency
+ ];
+ }
+ }
+ }
+
+ // The payment has been (partially) charged back.
+ // Let's process chargebacks (they have no states as refunds)
+ if ($mollie_payment->hasChargebacks()) {
+ foreach ($mollie_payment->chargebacks() as $chargeback) {
+ if ($chargeback->amount->value) {
+ $refunds[] = [
+ 'id' => $chargeback->id,
+ 'amount' => round(floatval($chargeback->amount->value) * 100),
+ 'type' => self::TYPE_CHARGEBACK,
+ // Note: we assume this is the original payment/wallet currency
+ ];
+ }
}
- } elseif ($mollie_payment->hasRefunds()) {
- // The payment has been (partially) refunded.
- // The status of the payment is still "paid"
- // TODO: Update balance
- } elseif ($mollie_payment->hasChargebacks()) {
- // The payment has been (partially) charged back.
- // The status of the payment is still "paid"
- // TODO: Update balance
}
} elseif ($mollie_payment->isFailed()) {
// Note: I didn't find a way to get any description of the problem with a payment
@@ -343,6 +366,10 @@
self::creditPayment($payment, $mollie_payment);
}
+ foreach ($refunds as $refund) {
+ $this->storeRefund($payment->wallet, $refund);
+ }
+
DB::commit();
if (!empty($notify)) {
diff --git a/src/app/Providers/PaymentProvider.php b/src/app/Providers/PaymentProvider.php
--- a/src/app/Providers/PaymentProvider.php
+++ b/src/app/Providers/PaymentProvider.php
@@ -2,6 +2,7 @@
namespace App\Providers;
+use App\Transaction;
use App\Payment;
use App\Wallet;
@@ -18,6 +19,8 @@
public const TYPE_ONEOFF = 'oneoff';
public const TYPE_RECURRING = 'recurring';
public const TYPE_MANDATE = 'mandate';
+ public const TYPE_REFUND = 'refund';
+ public const TYPE_CHARGEBACK = 'chargeback';
/** const int Minimum amount of money in a single payment (in cents) */
public const MIN_AMOUNT = 1000;
@@ -153,4 +156,42 @@
return $db_payment;
}
+
+ /**
+ * Deduct an amount of pecunia from the wallet.
+ * Creates a payment and transaction records for the refund/chargeback operation.
+ *
+ * @param \App\Wallet $wallet A wallet object
+ * @param array $refund A refund or chargeback data (id, type, amount, description)
+ *
+ * @return void
+ */
+ protected function storeRefund(Wallet $wallet, array $refund): void
+ {
+ if (empty($refund) || empty($refund['amount'])) {
+ return;
+ }
+
+ $wallet->balance -= $refund['amount'];
+ $wallet->save();
+
+ if ($refund['type'] == self::TYPE_CHARGEBACK) {
+ $transaction_type = Transaction::WALLET_CHARGEBACK;
+ } else {
+ $transaction_type = Transaction::WALLET_REFUND;
+ }
+
+ Transaction::create([
+ 'object_id' => $wallet->id,
+ 'object_type' => Wallet::class,
+ 'type' => $transaction_type,
+ 'amount' => $refund['amount'],
+ 'description' => $refund['description'] ?? '',
+ ]);
+
+ $refund['status'] = self::STATUS_PAID;
+ $refund['amount'] *= -1;
+
+ $this->storePayment($refund, $wallet->id);
+ }
}
diff --git a/src/app/Transaction.php b/src/app/Transaction.php
--- a/src/app/Transaction.php
+++ b/src/app/Transaction.php
@@ -28,6 +28,8 @@
public const WALLET_CREDIT = 'credit';
public const WALLET_DEBIT = 'debit';
public const WALLET_PENALTY = 'penalty';
+ public const WALLET_REFUND = 'refund';
+ public const WALLET_CHARGEBACK = 'chback';
protected $fillable = [
// actor, if any
@@ -94,6 +96,8 @@
case self::WALLET_CREDIT:
case self::WALLET_DEBIT:
case self::WALLET_PENALTY:
+ case self::WALLET_REFUND:
+ case self::WALLET_CHARGEBACK:
// TODO: This must be a wallet.
$this->attributes['type'] = $value;
break;
@@ -112,7 +116,9 @@
{
$label = $this->objectTypeToLabelString() . '-' . $this->{'type'} . '-short';
- return \trans("transactions.{$label}", $this->descriptionParams());
+ $result = \trans("transactions.{$label}", $this->descriptionParams());
+
+ return trim($result, ': ');
}
/**
diff --git a/src/resources/lang/en/documents.php b/src/resources/lang/en/documents.php
--- a/src/resources/lang/en/documents.php
+++ b/src/resources/lang/en/documents.php
@@ -32,6 +32,8 @@
'receipt-filename' => ":site Receipt for :id",
'receipt-title' => "Receipt for :month :year",
'receipt-item-desc' => ":site Services",
+ 'receipt-refund' => "Refund",
+ 'receipt-chargeback' => "Chargeback",
'subtotal' => "Subtotal",
'vat' => "VAT (:rate%)",
diff --git a/src/resources/lang/en/transactions.php b/src/resources/lang/en/transactions.php
--- a/src/resources/lang/en/transactions.php
+++ b/src/resources/lang/en/transactions.php
@@ -5,17 +5,22 @@
'entitlement-billed' => ':sku_title for :object is billed at :amount',
'entitlement-deleted' => ':user_email deleted :sku_title for :object',
+ 'entitlement-created-short' => 'Added :sku_title for :object',
+ 'entitlement-billed-short' => 'Billed :sku_title for :object',
+ 'entitlement-deleted-short' => 'Deleted :sku_title for :object',
+
'wallet-award' => 'Bonus of :amount awarded to :wallet; :description',
+ 'wallet-chargeback' => ':amount was charged back from :wallet',
'wallet-credit' => ':amount was added to the balance of :wallet',
'wallet-debit' => ':amount was deducted from the balance of :wallet',
'wallet-penalty' => 'The balance of :wallet was reduced by :amount; :description',
-
- 'entitlement-created-short' => 'Added :sku_title for :object',
- 'entitlement-billed-short' => 'Billed :sku_title for :object',
- 'entitlement-deleted-short' => 'Deleted :sku_title for :object',
+ 'wallet-refund' => ':amount was refunded from the balance of :wallet',
+ 'wallet-refund' => ':amount was refunded from :wallet',
'wallet-award-short' => 'Bonus: :description',
+ 'wallet-chargeback-short' => 'Chargeback',
'wallet-credit-short' => 'Payment',
'wallet-debit-short' => 'Deduction',
'wallet-penalty-short' => 'Charge: :description',
+ 'wallet-refund-short' => 'Refund: :description',
];
diff --git a/src/tests/Feature/Controller/PaymentsMollieTest.php b/src/tests/Feature/Controller/PaymentsMollieTest.php
--- a/src/tests/Feature/Controller/PaymentsMollieTest.php
+++ b/src/tests/Feature/Controller/PaymentsMollieTest.php
@@ -34,8 +34,12 @@
Payment::where('wallet_id', $wallet->id)->delete();
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();
+ $types = [
+ Transaction::WALLET_CREDIT,
+ Transaction::WALLET_REFUND,
+ Transaction::WALLET_CHARGEBACK,
+ ];
+ Transaction::where('object_id', $wallet->id)->whereIn('type', $types)->delete();
}
/**
@@ -48,8 +52,12 @@
Payment::where('wallet_id', $wallet->id)->delete();
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();
+ $types = [
+ Transaction::WALLET_CREDIT,
+ Transaction::WALLET_REFUND,
+ Transaction::WALLET_CHARGEBACK,
+ ];
+ Transaction::where('object_id', $wallet->id)->whereIn('type', $types)->delete();
parent::tearDown();
}
@@ -569,7 +577,162 @@
return $job_payment->id === $payment->id;
});
- $responseStack = $this->unmockMollie();
+ $this->unmockMollie();
+ }
+
+ /**
+ * Test refund/chargeback handling by the webhook
+ *
+ * @group mollie
+ */
+ public function testRefundAndChargeback(): void
+ {
+ Bus::fake();
+
+ $user = $this->getTestUser('john@kolab.org');
+ $wallet = $user->wallets()->first();
+ $wallet->transactions()->delete();
+
+ $mollie = PaymentProvider::factory('mollie');
+
+ // Create a paid payment
+ $payment = Payment::create([
+ 'id' => 'tr_123456',
+ 'status' => PaymentProvider::STATUS_PAID,
+ 'amount' => 123,
+ 'type' => PaymentProvider::TYPE_ONEOFF,
+ 'wallet_id' => $wallet->id,
+ 'provider' => 'mollie',
+ 'description' => 'test',
+ ]);
+
+ // Test handling a refund by the webhook
+
+ $mollie_response1 = [
+ "resource" => "payment",
+ "id" => $payment->id,
+ "status" => "paid",
+ // Status is not enough, paidAt is used to distinguish the state
+ "paidAt" => date('c'),
+ "mode" => "test",
+ "_links" => [
+ "refunds" => [
+ "href" => "https://api.mollie.com/v2/payments/{$payment->id}/refunds",
+ "type" => "application/hal+json"
+ ]
+ ]
+ ];
+
+ $mollie_response2 = [
+ "count" => 1,
+ "_links" => [],
+ "_embedded" => [
+ "refunds" => [
+ [
+ "resource" => "refund",
+ "id" => "re_123456",
+ "status" => \Mollie\Api\Types\RefundStatus::STATUS_REFUNDED,
+ "paymentId" => $payment->id,
+ "description" => "refund desc",
+ "amount" => [
+ "currency" => "CHF",
+ "value" => "1.01",
+ ],
+ ]
+ ]
+ ]
+ ];
+
+ // We'll trigger the webhook with payment id and use mocking for
+ // requests to the Mollie payments API.
+ $responseStack = $this->mockMollie();
+ $responseStack->append(new Response(200, [], json_encode($mollie_response1)));
+ $responseStack->append(new Response(200, [], json_encode($mollie_response2)));
+
+ $post = ['id' => $payment->id];
+ $response = $this->post("api/webhooks/payment/mollie", $post);
+ $response->assertStatus(200);
+
+ $wallet->refresh();
+
+ $this->assertEquals(-101, $wallet->balance);
+
+ $transactions = $wallet->transactions()->where('type', Transaction::WALLET_REFUND)->get();
+
+ $this->assertCount(1, $transactions);
+ $this->assertSame(101, $transactions[0]->amount);
+ $this->assertSame(Transaction::WALLET_REFUND, $transactions[0]->type);
+ $this->assertSame("refund desc", $transactions[0]->description);
+
+ $payments = $wallet->payments()->where('id', 're_123456')->get();
+
+ $this->assertCount(1, $payments);
+ $this->assertSame(-101, $payments[0]->amount);
+ $this->assertSame(PaymentProvider::STATUS_PAID, $payments[0]->status);
+ $this->assertSame(PaymentProvider::TYPE_REFUND, $payments[0]->type);
+ $this->assertSame("mollie", $payments[0]->provider);
+ $this->assertSame("refund desc", $payments[0]->description);
+
+ // Test handling a chargeback by the webhook
+
+ $mollie_response1["_links"] = [
+ "chargebacks" => [
+ "href" => "https://api.mollie.com/v2/payments/{$payment->id}/chargebacks",
+ "type" => "application/hal+json"
+ ]
+ ];
+
+ $mollie_response2 = [
+ "count" => 1,
+ "_links" => [],
+ "_embedded" => [
+ "chargebacks" => [
+ [
+ "resource" => "chargeback",
+ "id" => "chb_123456",
+ "paymentId" => $payment->id,
+ "amount" => [
+ "currency" => "CHF",
+ "value" => "0.15",
+ ],
+ ]
+ ]
+ ]
+ ];
+
+ // We'll trigger the webhook with payment id and use mocking for
+ // requests to the Mollie payments API.
+ $responseStack = $this->mockMollie();
+ $responseStack->append(new Response(200, [], json_encode($mollie_response1)));
+ $responseStack->append(new Response(200, [], json_encode($mollie_response2)));
+
+ $post = ['id' => $payment->id];
+ $response = $this->post("api/webhooks/payment/mollie", $post);
+ $response->assertStatus(200);
+
+ $wallet->refresh();
+
+ $this->assertEquals(-116, $wallet->balance);
+
+ $transactions = $wallet->transactions()->where('type', Transaction::WALLET_CHARGEBACK)->get();
+
+ $this->assertCount(1, $transactions);
+ $this->assertSame(15, $transactions[0]->amount);
+ $this->assertSame(Transaction::WALLET_CHARGEBACK, $transactions[0]->type);
+ $this->assertSame('', $transactions[0]->description);
+
+ $payments = $wallet->payments()->where('id', 'chb_123456')->get();
+
+ $this->assertCount(1, $payments);
+ $this->assertSame(-15, $payments[0]->amount);
+ $this->assertSame(PaymentProvider::STATUS_PAID, $payments[0]->status);
+ $this->assertSame(PaymentProvider::TYPE_CHARGEBACK, $payments[0]->type);
+ $this->assertSame("mollie", $payments[0]->provider);
+ $this->assertSame('', $payments[0]->description);
+
+ Bus::assertNotDispatched(\App\Jobs\PaymentEmail::class);
+
+ $this->unmockMollie();
}
/**
diff --git a/src/tests/Feature/Controller/WalletsTest.php b/src/tests/Feature/Controller/WalletsTest.php
--- a/src/tests/Feature/Controller/WalletsTest.php
+++ b/src/tests/Feature/Controller/WalletsTest.php
@@ -209,6 +209,7 @@
$this->assertSame('CHF', $json['currency']);
$this->assertSame($wallet->balance, $json['balance']);
$this->assertTrue(empty($json['description']));
+ // TODO: This assertion does not work after a longer while from seeding
$this->assertTrue(!empty($json['notice']));
}
diff --git a/src/tests/Feature/Documents/ReceiptTest.php b/src/tests/Feature/Documents/ReceiptTest.php
--- a/src/tests/Feature/Documents/ReceiptTest.php
+++ b/src/tests/Feature/Documents/ReceiptTest.php
@@ -62,7 +62,7 @@
// The main table content
$content = $dom->getElementById('content');
$records = $content->getElementsByTagName('tr');
- $this->assertCount(5, $records);
+ $this->assertCount(7, $records);
$headerCells = $records[0]->getElementsByTagName('th');
$this->assertCount(3, $headerCells);
@@ -81,13 +81,23 @@
$this->assertSame('0,01 CHF', $this->getNodeContent($cells[2]));
$cells = $records[3]->getElementsByTagName('td');
$this->assertCount(3, $cells);
- $this->assertSame('2020-05-31', $this->getNodeContent($cells[0]));
+ $this->assertSame('2020-05-21', $this->getNodeContent($cells[0]));
$this->assertSame("$appName Services", $this->getNodeContent($cells[1]));
$this->assertSame('1,00 CHF', $this->getNodeContent($cells[2]));
- $summaryCells = $records[4]->getElementsByTagName('td');
+ $cells = $records[4]->getElementsByTagName('td');
+ $this->assertCount(3, $cells);
+ $this->assertSame('2020-05-30', $this->getNodeContent($cells[0]));
+ $this->assertSame("Refund", $this->getNodeContent($cells[1]));
+ $this->assertSame('-1,00 CHF', $this->getNodeContent($cells[2]));
+ $cells = $records[5]->getElementsByTagName('td');
+ $this->assertCount(3, $cells);
+ $this->assertSame('2020-05-31', $this->getNodeContent($cells[0]));
+ $this->assertSame("Chargeback", $this->getNodeContent($cells[1]));
+ $this->assertSame('-0,10 CHF', $this->getNodeContent($cells[2]));
+ $summaryCells = $records[6]->getElementsByTagName('td');
$this->assertCount(2, $summaryCells);
$this->assertSame('Total', $this->getNodeContent($summaryCells[0]));
- $this->assertSame('13,35 CHF', $this->getNodeContent($summaryCells[1]));
+ $this->assertSame('12,25 CHF', $this->getNodeContent($summaryCells[1]));
// Customer data
$customer = $dom->getElementById('customer');
@@ -127,7 +137,7 @@
// The main table content
$content = $dom->getElementById('content');
$records = $content->getElementsByTagName('tr');
- $this->assertCount(7, $records);
+ $this->assertCount(9, $records);
$cells = $records[1]->getElementsByTagName('td');
$this->assertCount(3, $cells);
@@ -141,21 +151,31 @@
$this->assertSame('0,01 CHF', $this->getNodeContent($cells[2]));
$cells = $records[3]->getElementsByTagName('td');
$this->assertCount(3, $cells);
- $this->assertSame('2020-05-31', $this->getNodeContent($cells[0]));
+ $this->assertSame('2020-05-21', $this->getNodeContent($cells[0]));
$this->assertSame("$appName Services", $this->getNodeContent($cells[1]));
$this->assertSame('0,92 CHF', $this->getNodeContent($cells[2]));
- $subtotalCells = $records[4]->getElementsByTagName('td');
+ $cells = $records[4]->getElementsByTagName('td');
+ $this->assertCount(3, $cells);
+ $this->assertSame('2020-05-30', $this->getNodeContent($cells[0]));
+ $this->assertSame("Refund", $this->getNodeContent($cells[1]));
+ $this->assertSame('-0,92 CHF', $this->getNodeContent($cells[2]));
+ $cells = $records[5]->getElementsByTagName('td');
+ $this->assertCount(3, $cells);
+ $this->assertSame('2020-05-31', $this->getNodeContent($cells[0]));
+ $this->assertSame("Chargeback", $this->getNodeContent($cells[1]));
+ $this->assertSame('-0,09 CHF', $this->getNodeContent($cells[2]));
+ $subtotalCells = $records[6]->getElementsByTagName('td');
$this->assertCount(2, $subtotalCells);
$this->assertSame('Subtotal', $this->getNodeContent($subtotalCells[0]));
- $this->assertSame('12,32 CHF', $this->getNodeContent($subtotalCells[1]));
- $vatCells = $records[5]->getElementsByTagName('td');
+ $this->assertSame('11,31 CHF', $this->getNodeContent($subtotalCells[1]));
+ $vatCells = $records[7]->getElementsByTagName('td');
$this->assertCount(2, $vatCells);
$this->assertSame('VAT (7.7%)', $this->getNodeContent($vatCells[0]));
- $this->assertSame('1,03 CHF', $this->getNodeContent($vatCells[1]));
- $totalCells = $records[6]->getElementsByTagName('td');
+ $this->assertSame('0,94 CHF', $this->getNodeContent($vatCells[1]));
+ $totalCells = $records[8]->getElementsByTagName('td');
$this->assertCount(2, $totalCells);
$this->assertSame('Total', $this->getNodeContent($totalCells[0]));
- $this->assertSame('13,35 CHF', $this->getNodeContent($totalCells[1]));
+ $this->assertSame('12,25 CHF', $this->getNodeContent($totalCells[1]));
}
/**
@@ -196,7 +216,7 @@
// Create two payments out of the 2020-05 period
// and three in it, plus one in the period but unpaid,
- // and one with amount 0
+ // and one with amount 0, and an extra refund and chanrgeback
$payment = Payment::create([
'id' => 'AAA1',
@@ -246,7 +266,7 @@
$payment->updated_at = Carbon::create(2020, 5, 1, 0, 0, 0);
$payment->save();
- // ... so we expect the last three on the receipt
+ // ... so we expect the five three on the receipt
$payment = Payment::create([
'id' => 'AAA5',
'status' => PaymentProvider::STATUS_PAID,
@@ -280,6 +300,30 @@
'provider' => 'stripe',
'amount' => 100,
]);
+ $payment->updated_at = Carbon::create(2020, 5, 21, 23, 59, 0);
+ $payment->save();
+
+ $payment = Payment::create([
+ 'id' => 'ref1',
+ 'status' => PaymentProvider::STATUS_PAID,
+ 'type' => PaymentProvider::TYPE_REFUND,
+ 'description' => 'refund desc',
+ 'wallet_id' => $wallet->id,
+ 'provider' => 'stripe',
+ 'amount' => -100,
+ ]);
+ $payment->updated_at = Carbon::create(2020, 5, 30, 23, 59, 0);
+ $payment->save();
+
+ $payment = Payment::create([
+ 'id' => 'chback1',
+ 'status' => PaymentProvider::STATUS_PAID,
+ 'type' => PaymentProvider::TYPE_CHARGEBACK,
+ 'description' => '',
+ 'wallet_id' => $wallet->id,
+ 'provider' => 'stripe',
+ 'amount' => -10,
+ ]);
$payment->updated_at = Carbon::create(2020, 5, 31, 23, 59, 0);
$payment->save();
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Thu, Apr 2, 10:45 PM (6 h, 46 m ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18821326
Default Alt Text
D1762.1775169950.diff (25 KB)
Attached To
Mode
D1762: Mollie: Refunds/chargebacks handling
Attached
Detach File
Event Timeline