update lisensi prosess
tests / ci (push) Has been cancelled

This commit is contained in:
Wian Drs
2026-08-25 13:43:55 +07:00
parent 1e80be180e
commit 60946ddf37
58 changed files with 2903 additions and 165 deletions
+23
View File
@@ -0,0 +1,23 @@
<?php
namespace App\Billing\Contracts;
use App\Models\BillingOrder;
interface PaymentGateway
{
/** @return array<int,array{code:string,name:string,fee:int}> */
public function paymentMethods(BillingOrder $order): array;
/** @return array{external_id:?string,checkout_url:?string,metadata:array<string,mixed>} */
public function createCheckout(BillingOrder $order): array;
/** @param array<string,mixed> $payload */
public function verifyWebhook(array $payload, array $headers = []): bool;
/** @param array<string,mixed> $payload */
public function statusFromWebhook(array $payload): string;
/** @param array<string,mixed> $payload */
public function externalIdFromWebhook(array $payload): ?string;
}
@@ -0,0 +1,112 @@
<?php
namespace App\Billing\Gateways;
use App\Billing\Contracts\PaymentGateway;
use App\Models\BillingOrder;
use Illuminate\Http\Client\PendingRequest;
use Illuminate\Support\Facades\Http;
use RuntimeException;
class DuitkuPaymentGateway implements PaymentGateway
{
public function paymentMethods(BillingOrder $order): array
{
[$merchant, $key] = $this->credentials();
$amount = (int) $order->total;
$dateTime = now('Asia/Jakarta')->format('Y-m-d H:i:s');
$response = $this->http()->post(
$this->baseUrl().'/webapi/api/merchant/paymentmethod/getpaymentmethod',
[
'merchantcode' => $merchant,
'amount' => $amount,
'datetime' => $dateTime,
'signature' => hash_hmac('sha256', $merchant.$amount.$dateTime, $key),
]
);
if (! $response->successful() || ! is_array($response->json('paymentFee'))) {
throw new RuntimeException('PAYMENT_METHODS_REJECTED: '.($response->json('responseMessage') ?: 'Daftar kanal pembayaran Duitku tidak tersedia.'));
}
return collect($response->json('paymentFee'))->map(fn (array $method): array => [
'code' => (string) $method['paymentMethod'],
'name' => (string) $method['paymentName'],
'fee' => (int) ($method['totalFee'] ?? 0),
])->values()->all();
}
public function createCheckout(BillingOrder $order): array
{
[$merchant, $key] = $this->credentials();
if (! $order->payment_method) {
throw new RuntimeException('PAYMENT_METHOD_REQUIRED: Pilih kanal pembayaran terlebih dahulu.');
}
$externalId = 'RNDM-'.$order->uuid;
$user = $order->tenant->users()->where('is_active', true)->oldest()->firstOrFail();
$amount = (int) $order->total;
$payload = [
'merchantCode' => $merchant, 'paymentAmount' => $amount, 'merchantOrderId' => $externalId,
'paymentMethod' => $order->payment_method,
'productDetails' => 'Lisensi bulanan RADIQ NDM '.config("billing.plans.{$order->plan_code}.name"),
'email' => $user->email, 'customerVaName' => $user->name,
'callbackUrl' => route('payments.webhook', ['gateway' => 'duitku']),
'returnUrl' => route('checkout.return', $order->uuid), 'expiryPeriod' => 1440,
'signature' => hash_hmac('sha256', $merchant.$externalId.$amount, $key),
];
$response = $this->http()->post($this->baseUrl().'/webapi/api/merchant/v2/inquiry', $payload);
if (! $response->successful() || ! $response->json('paymentUrl')) {
throw new RuntimeException('PAYMENT_GATEWAY_REJECTED: '.($response->json('Message') ?: $response->json('message') ?: 'Duitku tidak menghasilkan URL pembayaran.'));
}
return ['external_id' => $externalId, 'checkout_url' => $response->json('paymentUrl'), 'metadata' => ['reference' => $response->json('reference')]];
}
public function verifyWebhook(array $payload, array $headers = []): bool
{
$merchant = (string) ($payload['merchantCode'] ?? '');
$amount = (string) ($payload['amount'] ?? '');
$orderId = (string) ($payload['merchantOrderId'] ?? '');
$signature = (string) ($payload['signature'] ?? '');
$expected = hash_hmac('sha256', $merchant.$amount.$orderId, (string) config('billing.duitku.api_key'));
return $merchant !== '' && hash_equals((string) config('billing.duitku.merchant_code'), $merchant) && hash_equals($expected, $signature);
}
public function statusFromWebhook(array $payload): string
{
return ($payload['resultCode'] ?? null) === '00' ? 'paid' : 'failed';
}
public function externalIdFromWebhook(array $payload): ?string
{
return isset($payload['merchantOrderId']) ? (string) $payload['merchantOrderId'] : null;
}
/** @return array{string,string} */
private function credentials(): array
{
$merchant = (string) config('billing.duitku.merchant_code');
$key = (string) config('billing.duitku.api_key');
if ($merchant === '' || $key === '') {
throw new RuntimeException('PAYMENT_GATEWAY_NOT_CONFIGURED: Merchant Code atau API Key Duitku belum diisi.');
}
return [$merchant, $key];
}
private function baseUrl(): string
{
return config('billing.duitku.sandbox') ? 'https://sandbox.duitku.com' : 'https://passport.duitku.com';
}
private function http(): PendingRequest
{
$request = Http::asJson()->acceptJson()->timeout(20);
$caBundle = config('billing.duitku.ca_bundle');
return is_string($caBundle) && $caBundle !== ''
? $request->withOptions(['verify' => $caBundle])
: $request;
}
}
@@ -0,0 +1,34 @@
<?php
namespace App\Billing\Gateways;
use App\Billing\Contracts\PaymentGateway;
use App\Models\BillingOrder;
class ManualPaymentGateway implements PaymentGateway
{
public function paymentMethods(BillingOrder $order): array
{
return [];
}
public function createCheckout(BillingOrder $order): array
{
return ['external_id' => 'MANUAL-'.$order->uuid, 'checkout_url' => null, 'metadata' => ['instruction' => 'Hubungi RADIQ untuk instruksi pembayaran. Aktivasi dilakukan setelah pembayaran diverifikasi.']];
}
public function verifyWebhook(array $payload, array $headers = []): bool
{
return false;
}
public function statusFromWebhook(array $payload): string
{
return 'pending';
}
public function externalIdFromWebhook(array $payload): ?string
{
return null;
}
}
+20
View File
@@ -0,0 +1,20 @@
<?php
namespace App\Billing;
use App\Billing\Contracts\PaymentGateway;
use InvalidArgumentException;
class PaymentGatewayManager
{
public function driver(?string $name = null): PaymentGateway
{
$name ??= config('billing.default_gateway');
$class = config("billing.gateways.{$name}");
if (! $class || ! is_a($class, PaymentGateway::class, true)) {
throw new InvalidArgumentException("Payment gateway [{$name}] tidak tersedia.");
}
return app($class);
}
}