Skip to content

به‌روزرسانی درایور باجت - #367

Open
saeeditbaz wants to merge 2 commits into
shetabit:masterfrom
saeeditbaz:feat/bajet-official-plugin-parity
Open

saeeditbaz wants to merge 2 commits into
shetabit:masterfrom
saeeditbaz:feat/bajet-official-plugin-parity

Conversation

@saeeditbaz

@saeeditbaz saeeditbaz commented Sep 21, 2026 •

Copy link
Copy Markdown
Contributor

به‌روزرسانی درایور باجت

  • اصلاح تأیید پرداخت با فراخوانی مستقیم verify و پشتیبانی از قالب‌های مختلف پاسخ موفق.
  • حفظ کنترل شناسه تراکنش و تطبیق مبلغ برگشتی با فاکتور؛ جلوگیری از پذیرش پاسخ نامعتبر یا پرداخت تأییدنشده.
  • اختیاری شدن شماره موبایل هنگام ایجاد سفارش و تبدیل صریح مبلغ ریال/تومان.
  • نگهداری توکن در همان نمونه درایور و نوسازی آن فقط یک بار پس از خطای 401 یا 403.
  • افزودن عملیات reverse، refund، refundInquiry و بررسی فعال بودن استرداد با isRefundEnabled.
  • ارسال trackId ثابت برای پیگیری استرداد و جلوگیری از تکرار خودکار درخواست در خطاهای ارتباطی.
  • به‌روزرسانی مستندات و تست‌های مسیرهای پرداخت، احراز هویت و استرداد.

نتیجه بررسی

۱۰۷ تست درایور باجت با پاسخ‌های شبیه‌سازی‌شده موفق بوده است. بررسی PHPStan نیز بدون خطا انجام شده است.

@codecov-commenter

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 94.33962% with 6 lines in your changes missing coverage. Please review.
⚠️ Please upload report for BASE (master@68ae732). Learn more about missing BASE report.

Files with missing lines Patch % Lines
src/Drivers/Bajet/Bajet.php 94.33% 6 Missing ⚠️
❗ Your organization needs to install the Codecov GitHub app to enable full functionality.
Additional details and impacted files
@@            Coverage Diff            @@
##             master     #367   +/-   ##
=========================================
  Coverage          ?   80.79%           
  Complexity        ?     1137           
=========================================
  Files             ?       66           
  Lines             ?     5716           
  Branches          ?        0           
=========================================
  Hits              ?     4618           
  Misses            ?     1098           
  Partials          ?        0           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.


private ?string $cachedToken = null;

private int $tokenExpiresAt = 0;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is not the state of driver but the state of token so we shouldn't keep it here!

for people using event-loop technologies in PHP, this line becomes a shared state and causes a race condition.

please keep the tokens expiration state either as a local variable or create a Token class for it:

readonly class Token
{
    public function __construct(
        public string $value,
        public int $expiresAt,
    ) {
    }
}

{
protected Client $client;

private ?string $cachedToken = null;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is also not needed! we should pass the token from caller to the callee. btw I can see there is only one call to token method and it is also a force token refresh request because it is called with a true parameter as ->token(true) , so please remove this line.

if ($credit > $amount || $cash !== $amount - $credit) {
throw new RuntimeException('Bajet paid amount does not match the invoice.');
}
} elseif ($status === '') {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When the response has a recognized success status (e.g. "success") but no amount, creditAmount, or cashAmount at all, this branch does nothing — verify() returns a receipt with zero amount corroboration against the invoice. Only the empty-status case is rejected here; a non-empty, recognized-success status with no amount data at all sails through. Was that intentional for some provider response shape, or should we require at least one amount field whenever we don't have a split?

$orderId = $this->text($result['orderId'] ?? null);
$expectedOrder = $this->invoice->getDetail('orderId');
if ($expectedOrder !== null && $orderId !== $this->text($expectedOrder)) {
if (array_key_exists('orderId', $result) && $expectedOrder !== null

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This changed from fail-closed to fail-open: previously a missing orderId in the response was treated as an empty string and rejected against $expectedOrder. Now, if the response simply omits orderId, the order-match check is skipped entirely rather than failing. Combined with the status-only path in verify() above, a minimal response like {"status":"success","referenceId":"ref-1"} would verify with no order or amount corroboration at all — worth double-checking this is the intended trust boundary for the official API.

private function httpsUrl(mixed $value): string
{
$url = $this->text($value);
$url = $this->text($value);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indentation regression: this line (and the return $url; a few lines below) picked up 4 extra spaces of indentation, so they're now misaligned with the rest of the method body. Looks unintentional — probably worth a formatter pass before merge.

}
if (($inquiry['finalStatus'] ?? null) !== 'SUCCESS') {
throw new RuntimeException('Bajet transaction is not ready for verification.');
if ($status !== '' && !in_array($status, ['success', 'successful', 'completed'], true)) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When both status and finalStatus are missing, $status is "" and this check is skipped. So a response with only creditAmount/cashAmount (no status at all) is treated as paid, and testOfficialVerificationShapesDoNotRequireInquiryOrOrderId even pins it (status => "", creditAmount => 10000).

Amounts that add up only say the numbers are consistent, not that the payment happened. Can we require an explicit success status here, or point to the official response sample that really has no status?

}

/** Amount is in invoice currency; reuse the same trackId when reconciling a refund. */
public function refund(?int $amount = null, ?string $trackId = null): array

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These new public methods (reverse, refund, refundInquiry, isRefundEnabled) are outside DriverInterface, so callers can only reach them by knowing the concrete class. Same thing we talked about in #179: please put them behind a small contract (e.g. Refundable) so other drivers can share it and callers can do $driver instanceof Refundable.


return $this->text($result['token'] ?? null);
$token = $this->text($result['token'] ?? null);
$ttl = min(840, $this->integer($result['expiresIn'] ?? 840));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: 840 is a magic number, and the token is treated as valid until the very last second. Give it a name (e.g. MAX_TOKEN_TTL) and maybe keep a few seconds of margin. This fits well in the Token class suggested above.

throw new RuntimeException('Unable to contact Bajet. Check transaction status before retrying.');
}

if ($refreshOnUnauthorized && in_array($response->getStatusCode(), [401, 403], true)) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A 403 usually means "not allowed", not "token expired". For order, reverse and refund we would refresh the token and send the same money request again. Should we retry only on 401, or at least only for requests that are safe to repeat? The PR says refunds are never auto-repeated, and this path is a small exception to it.

$refundCheck = $operation === 'terminal/check-refund';
if ($response->getStatusCode() < 200 || $response->getStatusCode() >= 300
|| (isset($body['status']) && is_numeric($body['status']) && (int) $body['status'] >= 400)
|| (array_key_exists('success', $body) ? $body['success'] !== true : !$refundCheck)) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This condition is hard to read: a success key, a numeric status, and a special case for terminal/check-refund, all in one expression. And the list at lines 294-296 repeats the same operation names. A small private method like isSuccessful(array $body, string $operation): bool plus one constant for the "no result envelope" operations would make it much easier to follow and test.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants