Compare commits

...

4 Commits

Author SHA1 Message Date
1873486d13 online payments for card order 2025-10-31 02:16:15 +05:00
5d3ea6d787 stan errors done 2025-10-31 01:21:00 +05:00
834527647d payment providers and some stan 2025-10-31 01:13:04 +05:00
11f99caf42 work on payments 2025-10-30 22:33:28 +05:00
16 changed files with 610 additions and 85 deletions

View File

@@ -3,8 +3,8 @@
namespace App\Filament\Clusters\Cards\CardOrders\Pages;
use App\Filament\Clusters\Cards\CardOrders\CardOrderResource;
use App\Modules\CardOrder\Repositories\CardOrderRepository;
use Filament\Resources\Pages\CreateRecord;
use Illuminate\Support\Facades\URL;
class CreateCardOrder extends CreateRecord
{
@@ -14,12 +14,14 @@ class CreateCardOrder extends CreateRecord
{
$defaultUrl = $this->getResource()::getUrl('index');
return $defaultUrl;
/** @var \App\Modules\CardOrder\Models\CardOrder */
$record = $this->record;
// $payment = (new OnlinePaymentRepo)->payCardOrder($resource);
$OnlinePayment = CardOrderRepository::make()
->createOnlinePaymentOrder($record);
// $payment['status'] === 'success'
// ? URL::remote($payment['url'])
// : $defaultUrl;
return $OnlinePayment->successful()
? $OnlinePayment->paymentLink()
: $defaultUrl;
}
}

View File

@@ -0,0 +1,10 @@
<?php
namespace App\Modules\AppHelpers\Contracts;
interface HasOnlinePaymentStatusFields
{
public function failed(): bool;
public function successful(): bool;
}

View File

@@ -77,4 +77,20 @@ class Branch extends Model
{
return $this->belongsToMany(User::class);
}
/**
* Get billing username
*/
public function billingUsername(): string
{
return $this->billing_username ?: '-';
}
/**
* Get billing password
*/
public function billingPassword(): string
{
return $this->billing_password ?: '-';
}
}

View File

@@ -59,6 +59,16 @@ class CardOrderModule implements ModuleContract
name: 'Branch',
message: 'This module is used for branches.',
),
new ModulePackage(
type: ModulePackageType::MODULE,
name: 'OrderStatus',
message: 'Supports orders.',
),
new ModulePackage(
type: ModulePackageType::MODULE,
name: 'OnlinePayment',
message: 'Is online payable.',
),
];
}

View File

@@ -6,6 +6,7 @@ use App\Models\User;
use App\Modules\Branch\Interfaces\BelongsToBranch;
use App\Modules\Branch\Models\Branch;
use App\Modules\LoanOrder\Repositories\LoanOrderRepository;
use App\Modules\OnlinePayment\Contracts\HasOnlinePayments;
use App\Modules\OrderStatus\Interfaces\HasStatus;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
@@ -111,4 +112,12 @@ class CardOrder extends Model implements BelongsToBranch, HasStatus
static::creating(LoanOrderRepository::creating());
static::created(LoanOrderRepository::created());
}
/**
* Price for card
*/
public function priceAmount(): int|float|string
{
return $this->cardState->price ?? 32;
}
}

View File

@@ -2,4 +2,32 @@
namespace App\Modules\CardOrder\Repositories;
class CardOrderRepository {}
use App\Modules\CardOrder\Models\CardOrder;
use App\Modules\HalkbankOnlinePayment\Repositories\HalkbankOnlinePaymentRepository;
use App\Modules\Makeable;
use App\Modules\OnlinePayment\Repositories\OnlinePaymentRepository;
class CardOrderRepository
{
use Makeable;
/**
* Create online payment order
*/
public function createOnlinePaymentOrder(CardOrder $record): OnlinePaymentRepository
{
/** @var \App\Modules\Branch\Models\Branch */
$branch = $record->branch;
return OnlinePaymentRepository::make(relatedModel: $record)
->paymentProvider(
HalkbankOnlinePaymentRepository::make()
->setUsername($branch->billingUsername())
->setPassword($branch->billingPassword())
->setAmount($record->priceAmount())
->setReturnUrl(route('halkbank-online-payment.store'))
->setDescription('Kart tölegi')
)
->sendRequest();
}
}

View File

@@ -0,0 +1,7 @@
<?php
return [
'username' => 'hello',
'password' => '',
'returnUrl' => '',
];

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Modules\HalkbankOnlinePayment\Controllers;
use App\Http\Controllers\Controller;
use Illuminate\Contracts\View\View;
use Illuminate\Http\Request;
class HalkbankOnlinePaymentController extends Controller
{
public function store(Request $request): View
{
$request->validate([
'orderId' => ['required', 'string', 'max:50', 'exists:online_payment_histories,orderId'],
]);
return view('welcome');
}
}

View File

@@ -0,0 +1,64 @@
<?php
namespace App\Modules\HalkbankOnlinePayment;
use App\Modules\Makeable;
use App\Modules\ModuleContract;
class HalkbankOnlinePaymentModule implements ModuleContract
{
use Makeable;
/**
* Module is enabled
*/
protected bool $enabled = true;
/**
* Check if is module enabled
*/
public function isEnabled(): bool
{
return $this->enabled;
}
/**
* Disable module
*/
public function disable(): void
{
$this->enabled = false;
}
/**
* Enable module
*/
public function enable(): void
{
$this->enabled = true;
}
/**
* Check if module has a filament resource
*/
public function hasFilamentResource(): bool
{
return false;
}
/**
* Get module composer requirements
*/
public function getComposerRequirements(): array
{
return [];
}
/**
* Get module composer suggestions
*/
public function getComposerSuggestions(): array
{
return [];
}
}

View File

@@ -0,0 +1,197 @@
<?php
namespace App\Modules\HalkbankOnlinePayment\Repositories;
use App\Modules\Makeable;
use App\Modules\OnlinePayment\Contracts\PaymentProviderContract;
use Exception;
use GuzzleHttp\Psr7\Response as GuzzleResponse;
use Illuminate\Http\Client\Response;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
class HalkbankOnlinePaymentRepository implements PaymentProviderContract
{
use Makeable;
public function __construct(
protected string $username = '',
protected string $password = '',
protected int|float|string $amount = '',
protected int|string $orderNumber = '',
protected string $returnUrl = '',
protected string $description = '',
) {
$this->username = config()->string('module.halkbank-online-payment.username');
$this->password = config()->string('module.halkbank-online-payment.password');
$this->returnUrl = config()->string('module.halkbank-online-payment.returnUrl');
$this->orderNumber = $this->generateOrderNumber();
}
/**
* Send request to gatewat
*/
public function sendRequest(): Response
{
try {
$paymentResponse = Http::get('https://mpi.gov.tm/payment/rest/register.do', [
'orderNumber' => $this->orderNumber,
'amount' => $this->formatAmount($this->amount),
'currency' => 934,
'language' => 'ru',
'userName' => $this->username,
'password' => $this->password,
'returnUrl' => $this->returnUrl,
'pageView' => 'DESKTOP',
'description' => $this->description,
]);
} catch (Exception $e) {
Config::set('logging.channels.halkbank_payment_error', [
'driver' => 'single',
'path' => storage_path('logs/halkbank_payment_error.log'),
'level' => 'debug',
]);
Log::channel('halkbank_payment_error')
->error('Payment error', [
'response' => [
'error' => $e->getMessage(),
'file_line' => $e->getFile().':'.$e->getLine(),
],
]);
return new Response(new GuzzleResponse(
503,
['Content-Type' => 'application/json'],
sprintf('{"errorCode":"1","errorMessage":"%s"}', $e->getMessage())
));
}
return $paymentResponse;
}
/**
* Format amount to match requirements
*/
public function formatAmount(int|float|string $amount): string
{
return number_format(floatval($amount), 2, '', '');
}
/**
* Get username
*/
public function username(): string
{
return $this->username;
}
/**
* Get password
*/
public function password(): string
{
return $this->password;
}
/**
* Get amount
*/
public function amount(): int|float|string
{
return $this->amount;
}
/**
* Get order number
*/
public function orderNumber(): int|string
{
return $this->orderNumber;
}
/**
* Set return url
*/
public function returnUrl(): string
{
return $this->returnUrl;
}
/**
* Set description
*/
public function description(): string
{
return $this->description;
}
/**
* Set username
*/
public function setUsername(string $username): self
{
$this->username = $username;
return $this;
}
/**
* Set password
*/
public function setPassword(string $password): self
{
$this->password = $password;
return $this;
}
/**
* Set amoutn
*/
public function setAmount(int|float|string $amount): self
{
$this->amount = $amount;
return $this;
}
/**
* Set order number
*/
public function setOrderNumber(int|string $orderNumber): self
{
$this->orderNumber = $orderNumber;
return $this;
}
/**
* Set return url
*/
public function setReturnUrl(string $returnUrl): self
{
$this->returnUrl = $returnUrl;
return $this;
}
/**
* Set description
*/
public function setDescription(string $description): self
{
$this->description = $description;
return $this;
}
/**
* Generate order number for payment
*/
public function generateOrderNumber(): string
{
return date('dmyHis');
}
}

View File

@@ -0,0 +1,7 @@
<?php
use App\Modules\HalkbankOnlinePayment\Controllers\HalkbankOnlinePaymentController;
use Illuminate\Support\Facades\Route;
Route::get('halkbank-online-payment-store', [HalkbankOnlinePaymentController::class, 'store'])
->name('halkbank-online-payment.store');

View File

@@ -0,0 +1,8 @@
<?php
namespace App\Modules\OnlinePayment\Contracts;
/**
* @property int $id
*/
interface HasOnlinePayments {}

View File

@@ -0,0 +1,20 @@
<?php
namespace App\Modules\OnlinePayment\Contracts;
use Illuminate\Http\Client\Response;
interface PaymentProviderContract
{
public function sendRequest(): Response;
public function orderNumber(): int|string;
public function amount(): int|float|string;
public function description(): string;
public function returnUrl(): string;
public function username(): string;
}

View File

@@ -26,7 +26,7 @@ use Illuminate\Database\Eloquent\Model;
* @property string $username
* @property int $online_paymantable_id
* @property string $online_paymantable_type
* @property ?array $api_response
* @property ?array<string, mixed> $api_response
* @property \Illuminate\Support\Carbon $created_at
* @property \Illuminate\Support\Carbon $updated_at
*/

View File

@@ -2,13 +2,18 @@
namespace App\Modules\OnlinePayment\Repositories;
use App\Modules\CardOrder\Models\CardOrder;
use App\Modules\AppHelpers\Contracts\HasOnlinePaymentStatusFields;
use App\Modules\Makeable;
use App\Modules\OnlinePayment\Contracts\HasOnlinePayments;
use App\Modules\OnlinePayment\Contracts\PaymentProviderContract;
use App\Modules\OnlinePayment\Models\OnlinePayment;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Http\Client\Response;
class OnlinePaymentRepository
class OnlinePaymentRepository implements HasOnlinePaymentStatusFields
{
use Makeable;
/**
* Pending online payments
*/
@@ -24,7 +29,47 @@ class OnlinePaymentRepository
*/
public const PAID = 'paid';
/**
/**
* Related model
*/
protected ?Model $relatedModel;
/**
* Payment provider
*/
protected PaymentProviderContract $provider;
/**
* Response
*/
protected Response $response;
/**
* If payment is successful
*/
protected bool $successful;
/**
* If payment has failed
*/
protected bool $failed;
/**
* Payment order id
*/
protected string $orderId;
/**
* Online payment link
*/
protected string $paymentLink;
public function __construct(?Model $relatedModel)
{
$this->relatedModel = $relatedModel;
}
/**
* Status Values
*
* @return array<null|string, string>
@@ -39,71 +84,6 @@ class OnlinePaymentRepository
];
}
/**
* Set price
*/
public function getPrice(int|float|string $price): string
{
return number_format(floatval($price), 2, '', '');
}
/**
* Pay card order
*
* @param mixed $resource
* @return array<string, string>
*/
public function payCardOrder()
{
$orderNumber = $this->generateOrderNumber();
// $paymentResponse = Http::get('https://mpi.gov.tm/payment/rest/register.do', [
// 'orderNumber' => $orderNumber,
// 'amount' => $this->getPrice($resource->priceAmount()),
// 'currency' => 934,
// 'language' => 'ru',
// 'userName' => $resource->branch->billing_username,
// 'password' => $resource->branch->billing_password,
// 'returnUrl' => route('online-payment-store'),
// 'pageView' => 'DESKTOP',
// 'description' => 'Kart tölegi',
// ])->onError(function ($response) {
// Log::channel('halkbank_payment_error')
// ->error('Payment error', [
// 'response' => [
// 'body' => $response->body(),
// ],
// ]);
// });
// if ($paymentResponse->failed()) {
// return [
// 'status' => 'failed',
// 'url' => '',
// ];
// }
// $onlinePaymentHistory = new OnlinePayment;
// $onlinePaymentHistory->online_paymantable_id = $resource->really();
// $onlinePaymentHistory->online_paymantable_type = CardOrder::class;
// $onlinePaymentHistory->amount = $resource->priceAmount();
// $onlinePaymentHistory->orderNumber = $orderNumber;
// $onlinePaymentHistory->description = 'Kart tölegi';
// $onlinePaymentHistory->orderId = $paymentResponse['orderId'];
// $onlinePaymentHistory->formUrl = $paymentResponse['formUrl'];
// $onlinePaymentHistory->successUrl = route('online-payment-store');
// $onlinePaymentHistory->errorUrl = route('online-payment-store');
// $onlinePaymentHistory->api_client = 'billing_username';
// $onlinePaymentHistory->username = $resource->branch->billing_username;
// $onlinePaymentHistory->paymentStatus = self::PENDING;
// $onlinePaymentHistory->save();
// return [
// 'status' => 'success',
// 'url' => $paymentResponse['formUrl'],
// ];
}
/**
* Generate order number for payment
*/
@@ -111,4 +91,96 @@ class OnlinePaymentRepository
{
return date('dmyHis');
}
/**
* Payment provder
*/
public function paymentProvider(PaymentProviderContract $provider): self
{
$this->provider = $provider;
return $this;
}
/**
* If payment has been successfull
*/
public function successful(): bool
{
return $this->successful;
}
/**
* If payment has been a failure
*/
public function failed(): bool
{
return $this->failed;
}
/**
* Set respond results
*/
public function setResponseResults(bool $result): void
{
$this->successful = $result;
$this->failed = ! $result;
}
/**
* Send request via provider
*/
public function sendRequest(): self
{
$this->response = $this->provider->sendRequest();
$this->failed = $this->response->failed();
$this->successful = $this->response->successful();
if ($this->response['errorCode'] != 0) {
$this->setResponseResults(false);
}
if ($this->successful) {
$this->orderId = string($this->response['orderId']);
$this->paymentLink = string($this->response['formUrl']);
$this->createHistory();
}
return $this;
}
/**
* Payment link
*/
public function paymentLink(): string
{
return $this->paymentLink;
}
/**
* Create online payment history
*/
public function createHistory(): void
{
$data = [
'amount' => $this->provider->amount(),
'orderNumber' => $this->provider->orderNumber(),
'description' => $this->provider->description(),
'orderId' => $this->orderId,
'formUrl' => $this->paymentLink,
'successUrl' => $this->provider->returnUrl(),
'errorUrl' => $this->provider->returnUrl(),
'username' => $this->provider->username(),
'paymentStatus' => self::PENDING,
];
if ($this->relatedModel) {
$data['online_paymantable_id'] = $this->relatedModel->id; // @phpstan-ignore-line
$data['online_paymantable_type'] = $this->relatedModel::class;
}
OnlinePayment::create($data);
}
}

View File

@@ -55,12 +55,41 @@ function module_exists(string $moduleName): bool
}
/**
* Create an anonymous class that acts as a dynamic object.
*
* @param mixed ...$arguments Key-value pairs passed as an associative array.
* @return object Anonymous class instance with dynamic properties.
* Cast to string
*/
function emptyClass(...$arguments): object
function string(mixed $value): string
{
if (! is_string($value)) {
throw new Exception('!!!Make it string!!!');
}
return $value;
}
/**
* Create an anonymous dynamic object with both properties and callable methods.
*
* You can define any mix of:
* - scalar or object properties
* - closures (which become methods bound to $this)
*
* Example:
* ```php
* $user = emptyClass(
* name: 'Ali',
* greet: fn(): string => "Hello, {$this->name}",
* setName: fn(string $n): void => $this->name = $n,
* );
*
* echo $user->greet(); // Hello, Ali
* $user->setName('Ahmet');
* echo $user->greet(); // Hello, Ahmet
* ```
*
*
* @param mixed ...$arguments Key-value pairs (property name => value or Closure)
*/
function emptyClass(mixed ...$arguments): object
{
/**
* @var array<string, mixed> $arguments
@@ -68,20 +97,25 @@ function emptyClass(...$arguments): object
return new class($arguments)
{
/**
* Internal data storage.
* Internal data store.
*
* @var array<string, mixed>
*/
private array $data = [];
/**
* Constructor that sets properties.
* Constructor that sets up dynamic properties and binds closures.
*
* @param array<string, mixed> $props
*/
public function __construct(array $props)
{
foreach ($props as $key => $value) {
if ($value instanceof Closure) {
/** @var Closure $value */
$value = $value->bindTo($this, self::class);
}
$this->data[$key] = $value;
}
}
@@ -111,5 +145,27 @@ function emptyClass(...$arguments): object
{
return isset($this->data[$key]);
}
/**
* Magic method caller invokes stored closures as methods.
*
* @param array<int, mixed> $args
*
* @throws BadMethodCallException if the method does not exist or is not callable.
*/
public function __call(string $method, array $args): mixed
{
$callable = $this->data[$method] ?? null;
if ($callable instanceof Closure) {
return $callable(...$args);
}
throw new BadMethodCallException(sprintf(
'Method %s() does not exist on %s.',
$method,
self::class
));
}
};
}