Skip to content

Commit 6df8504

Browse files
committed
Improve library with bug fixes, type safety, and new features
Bug fixes: - Fix typo: $pyamentMethodId → $paymentMethodId in Stripe adapter - Fix deletePaymentMethod() to validate response instead of always returning true - Fix off_session/confirm sent as string 'true' instead of boolean - Fix flatten() key collisions using array_replace instead of + - Fix Address getters removing unnecessary null coalescing on non-nullable props - Fix Address::getCity() return type from ?string to string Type safety improvements: - Return SetupIntent objects from future payment methods instead of raw arrays - Improve base Adapter::handleError() to use typed Exception with proper error codes (401→auth, 429→rate_limit, 5xx→api_error) instead of generic \Exception - Fix singular/plural mismatch: listFuturePayment → listFuturePayments in Pay facade New features: - Add pagination support (limit, startingAfter) to listCustomers and listPaymentMethods - Add validatePayment() in Adapter base class for currency/amount validation - Add getDispute() and submitDisputeEvidence() to Adapter, Stripe, and Pay facade https://claude.ai/code/session_01A28bsuCNWYbM1gS8oJLBRr
1 parent d00bfd3 commit 6df8504

5 files changed

Lines changed: 209 additions & 77 deletions

File tree

src/Pay/Adapter.php

Lines changed: 71 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use Utopia\Pay\Payment\Payment;
77
use Utopia\Pay\PaymentMethod\PaymentMethod;
88
use Utopia\Pay\Refund\Refund;
9+
use Utopia\Pay\SetupIntent\SetupIntent;
910

1011
abstract class Adapter
1112
{
@@ -201,9 +202,11 @@ abstract public function updatePaymentMethod(string $paymentMethodId, string $ty
201202
* List payment methods
202203
*
203204
* @param string $customerId Customer ID
205+
* @param int|null $limit Maximum number of results
206+
* @param string|null $startingAfter Cursor for pagination (ID of last item from previous page)
204207
* @return array<PaymentMethod> List of payment methods
205208
*/
206-
abstract public function listPaymentMethods(string $customerId): array;
209+
abstract public function listPaymentMethods(string $customerId, ?int $limit = null, ?string $startingAfter = null): array;
207210

208211
/**
209212
* Remove payment method
@@ -227,9 +230,11 @@ abstract public function createCustomer(string $name, string $email, ?Address $a
227230
/**
228231
* List customers
229232
*
233+
* @param int|null $limit Maximum number of results
234+
* @param string|null $startingAfter Cursor for pagination (ID of last item from previous page)
230235
* @return array<Customer> List of customers
231236
*/
232-
abstract public function listCustomers(): array;
237+
abstract public function listCustomers(?int $limit = null, ?string $startingAfter = null): array;
233238

234239
/**
235240
* Get customer details by ID
@@ -276,26 +281,26 @@ abstract public function getPaymentMethod(string $customerId, string $paymentMet
276281
* @param array<string> $paymentMethodTypes Allowed payment method types
277282
* @param array<string, mixed> $paymentMethodOptions Payment method options
278283
* @param string|null $paymentMethodConfiguration Payment method configuration ID
279-
* @return array<string, mixed> Setup intent data
284+
* @return SetupIntent The created setup intent
280285
*/
281-
abstract public function createFuturePayment(string $customerId, ?string $paymentMethod = null, array $paymentMethodTypes = [], array $paymentMethodOptions = [], ?string $paymentMethodConfiguration = null): array;
286+
abstract public function createFuturePayment(string $customerId, ?string $paymentMethod = null, array $paymentMethodTypes = [], array $paymentMethodOptions = [], ?string $paymentMethodConfiguration = null): SetupIntent;
282287

283288
/**
284289
* List future payments associated with the provided customer or payment method
285290
*
286291
* @param string|null $customerId Customer ID
287292
* @param string|null $paymentMethodId Payment method ID
288-
* @return array<array<string, mixed>> List of setup intents
293+
* @return array<SetupIntent> List of setup intents
289294
*/
290295
abstract public function listFuturePayments(?string $customerId = null, ?string $paymentMethodId = null): array;
291296

292297
/**
293298
* Get Future payment
294299
*
295300
* @param string $id Setup intent ID
296-
* @return array<string, mixed> Setup intent data
301+
* @return SetupIntent The setup intent
297302
*/
298-
abstract public function getFuturePayment(string $id): array;
303+
abstract public function getFuturePayment(string $id): SetupIntent;
299304

300305
/**
301306
* Update future payment setup
@@ -305,9 +310,9 @@ abstract public function getFuturePayment(string $id): array;
305310
* @param string|null $paymentMethod Payment method ID
306311
* @param array<string, mixed> $paymentMethodOptions Payment method options
307312
* @param string|null $paymentMethodConfiguration Payment method configuration ID
308-
* @return array<string, mixed> Updated setup intent data
313+
* @return SetupIntent The updated setup intent
309314
*/
310-
abstract public function updateFuturePayment(string $id, ?string $customerId = null, ?string $paymentMethod = null, array $paymentMethodOptions = [], ?string $paymentMethodConfiguration = null): array;
315+
abstract public function updateFuturePayment(string $id, ?string $customerId = null, ?string $paymentMethod = null, array $paymentMethodOptions = [], ?string $paymentMethodConfiguration = null): SetupIntent;
311316

312317
/**
313318
* Get mandate
@@ -328,6 +333,50 @@ abstract public function getMandate(string $id): array;
328333
*/
329334
abstract public function listDisputes(?int $limit = null, ?string $paymentIntentId = null, ?string $chargeId = null, ?int $createdAfter = null): array;
330335

336+
/**
337+
* Get a dispute by ID
338+
*
339+
* @param string $disputeId The dispute ID
340+
* @return array<string, mixed> The dispute data
341+
*/
342+
abstract public function getDispute(string $disputeId): array;
343+
344+
/**
345+
* Submit evidence for a dispute
346+
*
347+
* @param string $disputeId The dispute ID
348+
* @param array<string, mixed> $evidence Evidence data
349+
* @param bool $submit Whether to submit immediately (true) or save as draft (false)
350+
* @return array<string, mixed> The updated dispute data
351+
*/
352+
abstract public function submitDisputeEvidence(string $disputeId, array $evidence, bool $submit = true): array;
353+
354+
/**
355+
* Validate payment parameters before making an API call.
356+
*
357+
* @param int $amount Amount in smallest currency unit
358+
*
359+
* @throws Exception If validation fails
360+
*/
361+
protected function validatePayment(int $amount): void
362+
{
363+
if (! isset($this->currency) || empty($this->currency)) {
364+
throw new Exception(Exception::GENERAL_INVALID_REQUEST, 'Currency must be set before making payments');
365+
}
366+
367+
if (! Currency::isValid($this->currency)) {
368+
throw new Exception(Exception::CURRENCY_NOT_SUPPORTED, 'Invalid currency: '.$this->currency);
369+
}
370+
371+
if ($amount <= 0) {
372+
throw new Exception(Exception::AMOUNT_TOO_SMALL, 'Amount must be greater than zero');
373+
}
374+
375+
if (! Currency::meetsMinimum($amount, $this->currency)) {
376+
throw new Exception(Exception::AMOUNT_TOO_SMALL, 'Amount does not meet minimum for '.$this->currency);
377+
}
378+
}
379+
331380
/**
332381
* Call
333382
* Make a request
@@ -416,12 +465,20 @@ protected function call(string $method, string $url, array $params = [], array $
416465

417466
protected function handleError(int $code, mixed $response): void
418467
{
468+
$type = match (true) {
469+
$code === 401 => Exception::AUTHENTICATION_FAILED,
470+
$code === 429 => Exception::GENERAL_RATE_LIMIT,
471+
$code >= 500 => Exception::GENERAL_API_ERROR,
472+
default => Exception::GENERAL_UNKNOWN,
473+
};
474+
419475
if (is_array($response)) {
420-
/** @phpstan-ignore-next-line */
421-
throw new \Exception(json_encode($response), $code);
476+
$message = $response['message'] ?? $response['error']['message'] ?? json_encode($response);
477+
throw new Exception($type, $message, $code, $response);
422478
}
423479

424-
throw new \Exception($response, $code);
480+
$message = is_string($response) ? $response : 'Unknown error';
481+
throw new Exception($type, $message, $code);
425482
}
426483

427484
/**
@@ -431,15 +488,15 @@ protected function handleError(int $code, mixed $response): void
431488
* @param string $prefix
432489
* @return array<string, mixed>
433490
*/
434-
protected function flatten(array $data, $prefix = ''): array
491+
protected function flatten(array $data, string $prefix = ''): array
435492
{
436493
$output = [];
437494

438495
foreach ($data as $key => $value) {
439496
$finalKey = $prefix ? "{$prefix}[{$key}]" : $key;
440497

441498
if (is_array($value)) {
442-
$output += $this->flatten($value, $finalKey); // @todo: handle name collision here if needed
499+
$output = array_replace($output, $this->flatten($value, $finalKey));
443500
} else {
444501
$output[$finalKey] = $value;
445502
}

src/Pay/Adapter/Stripe.php

Lines changed: 72 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use Utopia\Pay\Payment\Payment;
1010
use Utopia\Pay\PaymentMethod\PaymentMethod;
1111
use Utopia\Pay\Refund\Refund;
12+
use Utopia\Pay\SetupIntent\SetupIntent;
1213

1314
class Stripe extends Adapter
1415
{
@@ -35,14 +36,15 @@ public function getName(): string
3536
*/
3637
public function purchase(int $amount, string $customerId, ?string $paymentMethodId = null, array $additionalParams = []): Payment
3738
{
39+
$this->validatePayment($amount);
3840
$path = '/payment_intents';
3941
$requestBody = [
4042
'amount' => $amount,
4143
'currency' => $this->currency,
4244
'customer' => $customerId,
4345
'payment_method' => $paymentMethodId,
44-
'off_session' => 'true',
45-
'confirm' => 'true',
46+
'off_session' => true,
47+
'confirm' => true,
4648
];
4749

4850
// Extract idempotency key if provided
@@ -64,15 +66,16 @@ public function purchase(int $amount, string $customerId, ?string $paymentMethod
6466
*/
6567
public function authorize(int $amount, string $customerId, ?string $paymentMethodId = null, array $additionalParams = []): Payment
6668
{
69+
$this->validatePayment($amount);
6770
$path = '/payment_intents';
6871
$requestBody = [
6972
'amount' => $amount,
7073
'currency' => $this->currency,
7174
'customer' => $customerId,
7275
'payment_method' => $paymentMethodId,
7376
'capture_method' => 'manual',
74-
'off_session' => 'true',
75-
'confirm' => 'true',
77+
'off_session' => true,
78+
'confirm' => true,
7679
];
7780

7881
// Extract idempotency key if provided
@@ -224,14 +227,21 @@ public function createPaymentMethod(string $customerId, string $type, array $pay
224227
}
225228

226229
/**
227-
* List cards
230+
* List payment methods
228231
*
229232
* @return array<PaymentMethod>
230233
*/
231-
public function listPaymentMethods(string $customerId): array
234+
public function listPaymentMethods(string $customerId, ?int $limit = null, ?string $startingAfter = null): array
232235
{
233236
$path = '/customers/'.$customerId.'/payment_methods';
234-
$result = $this->execute(self::METHOD_GET, $path);
237+
$params = [];
238+
if ($limit !== null) {
239+
$params['limit'] = $limit;
240+
}
241+
if ($startingAfter !== null) {
242+
$params['starting_after'] = $startingAfter;
243+
}
244+
$result = $this->execute(self::METHOD_GET, $path, $params);
235245

236246
$paymentMethods = [];
237247
foreach ($result['data'] ?? [] as $pm) {
@@ -297,9 +307,9 @@ public function updatePaymentMethod(string $paymentMethodId, string $type, array
297307
public function deletePaymentMethod(string $paymentMethodId): bool
298308
{
299309
$path = '/payment_methods/'.$paymentMethodId.'/detach';
300-
$this->execute(self::METHOD_POST, $path);
310+
$result = $this->execute(self::METHOD_POST, $path);
301311

302-
return true;
312+
return isset($result['id']) && $result['id'] === $paymentMethodId;
303313
}
304314

305315
/**
@@ -331,9 +341,16 @@ public function createCustomer(string $name, string $email, ?Address $address =
331341
*
332342
* @return array<Customer>
333343
*/
334-
public function listCustomers(): array
344+
public function listCustomers(?int $limit = null, ?string $startingAfter = null): array
335345
{
336-
$result = $this->execute(self::METHOD_GET, '/customers');
346+
$params = [];
347+
if ($limit !== null) {
348+
$params['limit'] = $limit;
349+
}
350+
if ($startingAfter !== null) {
351+
$params['starting_after'] = $startingAfter;
352+
}
353+
$result = $this->execute(self::METHOD_GET, '/customers', $params);
337354

338355
$customers = [];
339356
foreach ($result['data'] ?? [] as $customer) {
@@ -387,7 +404,7 @@ public function deleteCustomer(string $customerId): bool
387404
return $result['deleted'] ?? false;
388405
}
389406

390-
public function createFuturePayment(string $customerId, ?string $paymentMethod = null, array $paymentMethodTypes = ['card'], array $paymentMethodOptions = [], ?string $paymentMethodConfiguration = null): array
407+
public function createFuturePayment(string $customerId, ?string $paymentMethod = null, array $paymentMethodTypes = ['card'], array $paymentMethodOptions = [], ?string $paymentMethodConfiguration = null): SetupIntent
391408
{
392409
$path = '/setup_intents';
393410
$requestBody = [
@@ -402,7 +419,7 @@ public function createFuturePayment(string $customerId, ?string $paymentMethod =
402419
if ($paymentMethodConfiguration !== null) {
403420
$requestBody['payment_method_configuration'] = $paymentMethodConfiguration;
404421
$requestBody['automatic_payment_methods'] = [
405-
'enabled' => 'true',
422+
'enabled' => true,
406423
];
407424
unset($requestBody['payment_method_types']);
408425
}
@@ -413,33 +430,39 @@ public function createFuturePayment(string $customerId, ?string $paymentMethod =
413430

414431
$result = $this->execute(self::METHOD_POST, $path, $requestBody);
415432

416-
return $result;
433+
return SetupIntent::fromArray($result);
417434
}
418435

419-
public function getFuturePayment(string $id): array
436+
public function getFuturePayment(string $id): SetupIntent
420437
{
421438
$path = '/setup_intents/'.$id;
439+
$result = $this->execute(self::METHOD_GET, $path);
422440

423-
return $this->execute(self::METHOD_GET, $path);
441+
return SetupIntent::fromArray($result);
424442
}
425443

426-
public function listFuturePayments(?string $customerId = null, ?string $pyamentMethodId = null): array
444+
public function listFuturePayments(?string $customerId = null, ?string $paymentMethodId = null): array
427445
{
428446
$path = '/setup_intents';
429447
$requestBody = [];
430448
if ($customerId !== null) {
431449
$requestBody['customer'] = $customerId;
432450
}
433451

434-
if ($pyamentMethodId !== null) {
435-
$requestBody['payment_method'] = $pyamentMethodId;
452+
if ($paymentMethodId !== null) {
453+
$requestBody['payment_method'] = $paymentMethodId;
436454
}
437455
$result = $this->execute(self::METHOD_GET, $path, $requestBody);
438456

439-
return $result['data'];
457+
$setupIntents = [];
458+
foreach ($result['data'] ?? [] as $item) {
459+
$setupIntents[] = SetupIntent::fromArray($item);
460+
}
461+
462+
return $setupIntents;
440463
}
441464

442-
public function updateFuturePayment(string $id, ?string $customerId = null, ?string $paymentMethod = null, array $paymentMethodOptions = [], ?string $paymentMethodConfiguration = null): array
465+
public function updateFuturePayment(string $id, ?string $customerId = null, ?string $paymentMethod = null, array $paymentMethodOptions = [], ?string $paymentMethodConfiguration = null): SetupIntent
443466
{
444467
$path = '/setup_intents/'.$id;
445468
$requestBody = [];
@@ -456,7 +479,9 @@ public function updateFuturePayment(string $id, ?string $customerId = null, ?str
456479
$requestBody['payment_method_options'] = $paymentMethodOptions;
457480
}
458481

459-
return $this->execute(self::METHOD_POST, $path, $requestBody);
482+
$result = $this->execute(self::METHOD_POST, $path, $requestBody);
483+
484+
return SetupIntent::fromArray($result);
460485
}
461486

462487
/**
@@ -507,6 +532,31 @@ public function listDisputes(?int $limit = null, ?string $paymentIntentId = null
507532
return $result['data'];
508533
}
509534

535+
/**
536+
* Get a dispute by ID
537+
*/
538+
public function getDispute(string $disputeId): array
539+
{
540+
$path = '/disputes/'.$disputeId;
541+
542+
return $this->execute(self::METHOD_GET, $path);
543+
}
544+
545+
/**
546+
* Submit evidence for a dispute
547+
*/
548+
public function submitDisputeEvidence(string $disputeId, array $evidence, bool $submit = true): array
549+
{
550+
$path = '/disputes/'.$disputeId;
551+
$requestBody = ['evidence' => $evidence];
552+
553+
if ($submit) {
554+
$requestBody['submit'] = true;
555+
}
556+
557+
return $this->execute(self::METHOD_POST, $path, $requestBody);
558+
}
559+
510560
/**
511561
* Execute
512562
*

0 commit comments

Comments
 (0)