Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,29 @@ All notable changes to this project are documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Fixed

- `Payment` DTO now matches the current Whop payments schema. It previously
**required** a flat integer `amount` field that no longer exists in the API,
causing `payments->get()` and `->refund()` to throw `MissingArgumentsException`
on every real response.

### Changed (breaking)

- `Payment` no longer exposes `amount`. It now exposes the actual nullable
monetary fields `total`, `subtotal`, `usdTotal`, and `refundedAmount` (all
`?float`).
- `Payment::$applicationFee` is now read from the `application_fee` **object**
(`application_fee.amount`), falling back to a flat numeric `application_fee`
and then `application_fee_amount`. Its type changed from `?int` to `?float`.
- `Payment::$subStatus` is now read from `substatus` (the current field name),
falling back to the legacy `sub_status`.
- `RefundResponse::$amount` changed from `?int` to `?float`, so fractional
refund amounts (the API returns `number`) are no longer silently coerced to
`null` by an int-only type guard.

## [0.0.1] - 2026-05-15

Initial release — a framework-agnostic PHP SDK for the Whop API, extracted from the
Expand Down
63 changes: 49 additions & 14 deletions src/Dto/Payment/Payment.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,28 @@ private function __construct(
public string $id,
public string $status,
public ?string $subStatus,
public int $amount,
public ?float $total,
public ?float $subtotal,
public ?float $usdTotal,
public ?float $refundedAmount,
public string $currency,
public ?int $applicationFee,
public ?float $applicationFee,
public array $metadata,
public ?string $createdAt,
) {
}

/**
* Hydrate a Payment from a Whop API response.
*
* The monetary value lives in `total`/`subtotal`/`usd_total` (numbers,
* nullable) — there is no flat `amount` field. `status` carries the
* machine status and `substatus` the friendly one. `application_fee` is an
* object whose `amount` holds the fee.
*
* @param array<string, mixed> $data
*
* @throws MissingArgumentsException when id, status, or currency are absent
*/
public static function create(array $data): self
{
Expand All @@ -40,35 +52,58 @@ public static function create(array $data): self
throw MissingArgumentsException::forField(context: 'Payment response', field: 'status');
}

$amount = $data['amount'] ?? null;

if (!\is_int($amount)) {
throw MissingArgumentsException::forField(context: 'Payment response', field: 'amount');
}

$currencyRaw = $data['currency'] ?? null;

if (!\is_string($currencyRaw) || '' === $currencyRaw) {
throw MissingArgumentsException::forField(context: 'Payment response', field: 'currency');
}

$subStatus = $data['sub_status'] ?? null;
$applicationFee = $data['application_fee'] ?? $data['application_fee_amount'] ?? null;
$metadataRaw = $data['metadata'] ?? [];
// Whop renamed `sub_status` to `substatus`; accept both spellings so the
// SDK tolerates either, preferring the current one.
$subStatus = $data['substatus'] ?? $data['sub_status'] ?? null;

$createdAt = $data['created_at'] ?? null;

return new self(
id: $id,
status: $status,
subStatus: \is_string($subStatus) ? $subStatus : null,
amount: $amount,
total: self::toFloatOrNull($data['total'] ?? null),
subtotal: self::toFloatOrNull($data['subtotal'] ?? null),
usdTotal: self::toFloatOrNull($data['usd_total'] ?? null),
refundedAmount: self::toFloatOrNull($data['refunded_amount'] ?? null),
currency: strtoupper($currencyRaw),
applicationFee: \is_int($applicationFee) ? $applicationFee : null,
metadata: self::toStringKeyedArray($metadataRaw),
applicationFee: self::resolveApplicationFee($data),
metadata: self::toStringKeyedArray($data['metadata'] ?? []),
createdAt: \is_string($createdAt) ? $createdAt : null,
);
}

/**
* Resolve the application fee amount. The current API nests it inside an
* `application_fee` object ({id, amount, ...}); a flat numeric
* `application_fee` and an `application_fee_amount` sibling are also
* accepted for resilience, in that order of precedence.
*
* @param array<string, mixed> $data
*/
private static function resolveApplicationFee(array $data): ?float
{
$applicationFee = $data['application_fee'] ?? null;

if (\is_array($applicationFee)) {
return self::toFloatOrNull($applicationFee['amount'] ?? null);
}

return self::toFloatOrNull($applicationFee ?? $data['application_fee_amount'] ?? null);
}

private static function toFloatOrNull(mixed $value): ?float
{
// int values widen to float at the ?float return/property boundary.
return \is_int($value) || \is_float($value) ? $value : null;
}

/**
* @return array<string, mixed>
*/
Expand Down
4 changes: 2 additions & 2 deletions src/Dto/Payment/RefundResponse.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ private function __construct(
public string $refundId,
public ?string $paymentId,
public ?string $status,
public ?int $amount,
public ?float $amount,
) {
}

Expand All @@ -35,7 +35,7 @@ public static function fromResponse(array $data): self
refundId: $refundId,
paymentId: \is_string($paymentId) ? $paymentId : null,
status: \is_string($status) ? $status : null,
amount: \is_int($amount) ? $amount : null,
amount: \is_int($amount) || \is_float($amount) ? $amount : null,
);
}
}
Loading
Loading