mirror of
https://github.com/opensourcepos/opensourcepos.git
synced 2026-05-25 08:44:42 -04:00
Compare commits
1 Commits
unstable
...
feature/pa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4d8e2442b5 |
@@ -11,6 +11,10 @@ $routes->get('/', 'Login::index');
|
||||
$routes->get('login', 'Login::index');
|
||||
$routes->post('login', 'Login::index');
|
||||
|
||||
// Payment provider webhook routes (no authentication required)
|
||||
$routes->post('payments/webhook/(:segment)', 'Payments\Webhook::handle/$1');
|
||||
$routes->get('payments/status/(:segment)/(:segment)', 'Payments\Webhook::status/$1/$2');
|
||||
|
||||
$routes->add('no_access/index/(:segment)', 'No_access::index/$1');
|
||||
$routes->add('no_access/index/(:segment)/(:segment)', 'No_access::index/$1/$2');
|
||||
|
||||
|
||||
71
app/Controllers/Payments/Webhook.php
Normal file
71
app/Controllers/Payments/Webhook.php
Normal file
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers\Payments;
|
||||
|
||||
use App\Controllers\BaseController;
|
||||
use App\Libraries\Payments\PaymentProviderRegistry;
|
||||
use CodeIgniter\HTTP\ResponseInterface;
|
||||
|
||||
class Webhook extends BaseController
|
||||
{
|
||||
public function handle(string $providerId): ResponseInterface
|
||||
{
|
||||
$provider = PaymentProviderRegistry::getInstance()->getProvider($providerId);
|
||||
|
||||
if ($provider === null) {
|
||||
log_message('error', "Webhook received for unknown provider: {$providerId}");
|
||||
return $this->response->setStatusCode(404)->setJSON([
|
||||
'success' => false,
|
||||
'error' => 'Provider not found'
|
||||
]);
|
||||
}
|
||||
|
||||
$rawInput = $this->request->getBody();
|
||||
$data = json_decode($rawInput, true) ?? [];
|
||||
|
||||
if (empty($rawInput)) {
|
||||
$data = $this->request->getPost();
|
||||
}
|
||||
|
||||
try {
|
||||
$result = $provider->processCallback($data);
|
||||
|
||||
if ($result['success'] ?? false) {
|
||||
log_message('info', "Webhook processed successfully for provider: {$providerId}", $result);
|
||||
return $this->response->setStatusCode(200)->setJSON($result);
|
||||
}
|
||||
|
||||
log_message('warning', "Webhook processing failed for provider: {$providerId}", $result);
|
||||
return $this->response->setStatusCode(400)->setJSON($result);
|
||||
} catch (\Exception $e) {
|
||||
log_message('error', "Webhook exception for provider {$providerId}: " . $e->getMessage());
|
||||
return $this->response->setStatusCode(500)->setJSON([
|
||||
'success' => false,
|
||||
'error' => 'Internal server error'
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function status(string $providerId, string $transactionId): ResponseInterface
|
||||
{
|
||||
$provider = PaymentProviderRegistry::getInstance()->getProvider($providerId);
|
||||
|
||||
if ($provider === null) {
|
||||
return $this->response->setStatusCode(404)->setJSON([
|
||||
'success' => false,
|
||||
'error' => 'Provider not found'
|
||||
]);
|
||||
}
|
||||
|
||||
try {
|
||||
$result = $provider->getPaymentStatus($transactionId);
|
||||
return $this->response->setStatusCode(200)->setJSON($result);
|
||||
} catch (\Exception $e) {
|
||||
log_message('error', "Status check exception for provider {$providerId}: " . $e->getMessage());
|
||||
return $this->response->setStatusCode(500)->setJSON([
|
||||
'success' => false,
|
||||
'error' => 'Internal server error'
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,7 @@ use App\Models\Stock_location;
|
||||
use App\Models\Tokens\Token_invoice_count;
|
||||
use App\Models\Tokens\Token_customer;
|
||||
use App\Models\Tokens\Token_invoice_sequence;
|
||||
use CodeIgniter\Events\Events;
|
||||
use CodeIgniter\HTTP\ResponseInterface;
|
||||
use Config\Services;
|
||||
use Config\OSPOS;
|
||||
@@ -471,6 +472,13 @@ class Sales extends Secure_Controller
|
||||
}
|
||||
}
|
||||
|
||||
Events::trigger('payment_initiated', [
|
||||
'payment_type' => $payment_type,
|
||||
'amount' => $amount_tendered ?? 0,
|
||||
'sale_id' => $this->sale_lib->get_sale_id(),
|
||||
'customer_id' => $this->sale_lib->get_customer(),
|
||||
]);
|
||||
|
||||
return $this->_reload($data);
|
||||
}
|
||||
|
||||
@@ -786,6 +794,16 @@ class Sales extends Secure_Controller
|
||||
$data['error_message'] = lang('Sales.transaction_failed');
|
||||
} else {
|
||||
$data['barcode'] = $this->barcode_lib->generate_receipt_barcode($data['sale_id']);
|
||||
|
||||
Events::trigger('sale_completed', [
|
||||
'sale_id' => $data['sale_id_num'],
|
||||
'customer_id' => $customer_id,
|
||||
'employee_id' => $employee_id,
|
||||
'total' => $data['total'],
|
||||
'payments' => $data['payments'],
|
||||
'sale_type' => $sale_type,
|
||||
]);
|
||||
|
||||
$this->sale_lib->clear_all();
|
||||
return view('sales/' . $invoice_view, $data);
|
||||
}
|
||||
@@ -869,6 +887,16 @@ class Sales extends Secure_Controller
|
||||
$data['error_message'] = lang('Sales.transaction_failed');
|
||||
} else {
|
||||
$data['barcode'] = $this->barcode_lib->generate_receipt_barcode($data['sale_id']);
|
||||
|
||||
Events::trigger('sale_completed', [
|
||||
'sale_id' => $data['sale_id_num'],
|
||||
'customer_id' => $customer_id,
|
||||
'employee_id' => $employee_id,
|
||||
'total' => $data['total'],
|
||||
'payments' => $data['payments'],
|
||||
'sale_type' => $sale_type,
|
||||
]);
|
||||
|
||||
$this->sale_lib->clear_all();
|
||||
return view('sales/receipt', $data);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
namespace App\Database\Migrations;
|
||||
|
||||
use CodeIgniter\Database\Migration;
|
||||
|
||||
class Migration_PaymentTransactions extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
$forge = \Config\Services::forge();
|
||||
|
||||
$forge->addField([
|
||||
'id' => [
|
||||
'type' => 'INT',
|
||||
'constraint' => 11,
|
||||
'unsigned' => true,
|
||||
'auto_increment' => true
|
||||
],
|
||||
'provider_id' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 100,
|
||||
'null' => false
|
||||
],
|
||||
'sale_id' => [
|
||||
'type' => 'INT',
|
||||
'constraint' => 11,
|
||||
'unsigned' => true,
|
||||
'null' => true
|
||||
],
|
||||
'transaction_id' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 255,
|
||||
'null' => false
|
||||
],
|
||||
'amount' => [
|
||||
'type' => 'DECIMAL',
|
||||
'constraint' => '15,2',
|
||||
'null' => false
|
||||
],
|
||||
'currency' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 3,
|
||||
'default' => 'USD',
|
||||
'null' => false
|
||||
],
|
||||
'status' => [
|
||||
'type' => 'ENUM',
|
||||
'constraint' => ['pending', 'authorized', 'completed', 'failed', 'refunded', 'cancelled'],
|
||||
'default' => 'pending',
|
||||
'null' => false
|
||||
],
|
||||
'metadata' => [
|
||||
'type' => 'JSON',
|
||||
'null' => true
|
||||
],
|
||||
'created_at' => [
|
||||
'type' => 'TIMESTAMP',
|
||||
'null' => true
|
||||
],
|
||||
'updated_at' => [
|
||||
'type' => 'TIMESTAMP',
|
||||
'null' => true
|
||||
]
|
||||
]);
|
||||
|
||||
$forge->addKey('id', true);
|
||||
$forge->addKey('provider_id');
|
||||
$forge->addKey('sale_id');
|
||||
$forge->addKey('transaction_id');
|
||||
$forge->addKey('status');
|
||||
|
||||
$forge->createTable('payment_transactions', true);
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
$forge = \Config\Services::forge();
|
||||
$forge->dropTable('payment_transactions', true);
|
||||
}
|
||||
}
|
||||
58
app/Events/PaymentEvents.php
Normal file
58
app/Events/PaymentEvents.php
Normal file
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
use App\Libraries\Payments\PaymentProviderRegistry;
|
||||
use CodeIgniter\Events\Events;
|
||||
use Config\Services;
|
||||
|
||||
class PaymentEvents
|
||||
{
|
||||
public static function initialize(): void
|
||||
{
|
||||
Events::on('payment_initiated', [static::class, 'onPaymentInitiated']);
|
||||
Events::on('payment_completed', [static::class, 'onPaymentCompleted']);
|
||||
Events::on('payment_failed', [static::class, 'onPaymentFailed']);
|
||||
Events::on('sale_completed', [static::class, 'onSaleCompleted']);
|
||||
}
|
||||
|
||||
public static function onPaymentInitiated(array $data): void
|
||||
{
|
||||
log_message('debug', sprintf(
|
||||
'Payment initiated: type=%s, amount=%s, sale_id=%s',
|
||||
$data['payment_type'] ?? 'unknown',
|
||||
$data['amount'] ?? 0,
|
||||
$data['sale_id'] ?? 'pending'
|
||||
));
|
||||
}
|
||||
|
||||
public static function onPaymentCompleted(array $data): void
|
||||
{
|
||||
log_message('debug', sprintf(
|
||||
'Payment completed: type=%s, amount=%s, sale_id=%s',
|
||||
$data['payment_type'] ?? 'unknown',
|
||||
$data['amount'] ?? 0,
|
||||
$data['sale_id'] ?? 'pending'
|
||||
));
|
||||
}
|
||||
|
||||
public static function onPaymentFailed(array $data): void
|
||||
{
|
||||
log_message('warning', sprintf(
|
||||
'Payment failed: type=%s, amount=%s, error=%s',
|
||||
$data['payment_type'] ?? 'unknown',
|
||||
$data['amount'] ?? 0,
|
||||
$data['error'] ?? 'unknown error'
|
||||
));
|
||||
}
|
||||
|
||||
public static function onSaleCompleted(array $data): void
|
||||
{
|
||||
log_message('info', sprintf(
|
||||
'Sale completed: sale_id=%s, total=%s, payments=%s',
|
||||
$data['sale_id'] ?? 'unknown',
|
||||
$data['total'] ?? 0,
|
||||
json_encode($data['payments'] ?? [])
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Employee;
|
||||
use CodeIgniter\Events\Events;
|
||||
use Config\OSPOS;
|
||||
|
||||
/**
|
||||
@@ -276,6 +277,12 @@ function get_payment_options(): array
|
||||
$payments[lang('Sales.upi')] = lang('Sales.upi');
|
||||
}
|
||||
|
||||
// Allow payment provider plugins to add additional payment options
|
||||
$eventPayments = Events::trigger('payment_options', $payments);
|
||||
if (is_array($eventPayments)) {
|
||||
return $eventPayments;
|
||||
}
|
||||
|
||||
return $payments;
|
||||
}
|
||||
|
||||
|
||||
64
app/Helpers/payment_helper.php
Normal file
64
app/Helpers/payment_helper.php
Normal file
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
use App\Libraries\Payments\PaymentProviderRegistry;
|
||||
use CodeIgniter\Events\Events;
|
||||
|
||||
if (!function_exists('register_payment_provider')) {
|
||||
function register_payment_provider(App\Libraries\Payments\PaymentProviderInterface $provider): void
|
||||
{
|
||||
PaymentProviderRegistry::getInstance()->register($provider);
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('get_payment_providers')) {
|
||||
function get_payment_providers(): array
|
||||
{
|
||||
return PaymentProviderRegistry::getInstance()->getProviders();
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('get_enabled_payment_providers')) {
|
||||
function get_enabled_payment_providers(): array
|
||||
{
|
||||
return PaymentProviderRegistry::getInstance()->getEnabledProviders();
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('get_enabled_payment_types')) {
|
||||
function get_enabled_payment_types(): array
|
||||
{
|
||||
return PaymentProviderRegistry::getInstance()->getEnabledPaymentTypes();
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('get_payment_provider')) {
|
||||
function get_payment_provider(string $providerId): ?App\Libraries\Payments\PaymentProviderInterface
|
||||
{
|
||||
return PaymentProviderRegistry::getInstance()->getProvider($providerId);
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('get_payment_provider_for_type')) {
|
||||
function get_payment_provider_for_type(string $paymentTypeKey): ?App\Libraries\Payments\PaymentProviderInterface
|
||||
{
|
||||
return PaymentProviderRegistry::getInstance()->getProviderForPaymentType($paymentTypeKey);
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('payment_provider_content')) {
|
||||
function payment_provider_content(string $section, array $data = []): string
|
||||
{
|
||||
$results = Events::trigger("payment_view:{$section}", $data);
|
||||
$output = '';
|
||||
if (is_array($results)) {
|
||||
foreach ($results as $result) {
|
||||
if (is_string($result)) {
|
||||
$output .= $result;
|
||||
}
|
||||
}
|
||||
} elseif (is_string($results)) {
|
||||
$output = $results;
|
||||
}
|
||||
return $output;
|
||||
}
|
||||
}
|
||||
149
app/Libraries/Payments/PaymentProviderBase.php
Normal file
149
app/Libraries/Payments/PaymentProviderBase.php
Normal file
@@ -0,0 +1,149 @@
|
||||
<?php
|
||||
|
||||
namespace App\Libraries\Payments;
|
||||
|
||||
use CodeIgniter\Events\Events;
|
||||
|
||||
abstract class PaymentProviderBase implements PaymentProviderInterface
|
||||
{
|
||||
protected array $config = [];
|
||||
|
||||
public function __construct(array $config = [])
|
||||
{
|
||||
$this->config = $config;
|
||||
}
|
||||
|
||||
public function getProviderId(): string
|
||||
{
|
||||
return static::class;
|
||||
}
|
||||
|
||||
public function getVersion(): string
|
||||
{
|
||||
return '1.0.0';
|
||||
}
|
||||
|
||||
public function getProviderDescription(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function getPaymentTypes(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function getIcon(?string $paymentType = null): ?string
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getConfigView(): ?string
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public function isAvailable(): bool
|
||||
{
|
||||
$settings = $this->getSettings();
|
||||
return !empty($settings['enabled']) && $settings['enabled'] === '1';
|
||||
}
|
||||
|
||||
public function getSettings(): array
|
||||
{
|
||||
$settingsModel = model(\App\Models\Appconfig::class);
|
||||
$prefix = $this->getSettingsPrefix();
|
||||
|
||||
$settings = [];
|
||||
$result = $settingsModel->like('key', $prefix . '_')->findAll();
|
||||
|
||||
foreach ($result as $row) {
|
||||
$key = str_replace($prefix . '_', '', $row['key']);
|
||||
$settings[$key] = $row['value'];
|
||||
}
|
||||
|
||||
return $settings;
|
||||
}
|
||||
|
||||
public function saveSettings(array $settings): bool
|
||||
{
|
||||
$settingsModel = model(\App\Models\Appconfig::class);
|
||||
$prefix = $this->getSettingsPrefix();
|
||||
|
||||
foreach ($settings as $key => $value) {
|
||||
$fullKey = $prefix . '_' . $key;
|
||||
$settingsModel->save(['key' => $fullKey, 'value' => $value]);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function getSettingsPrefix(): string
|
||||
{
|
||||
return 'payment_' . $this->getProviderId();
|
||||
}
|
||||
|
||||
protected function logTransaction(
|
||||
string $transactionId,
|
||||
string $status,
|
||||
float $amount,
|
||||
string $currency = 'USD',
|
||||
?int $saleId = null,
|
||||
array $metadata = []
|
||||
): bool {
|
||||
$transactionModel = model(\App\Models\PaymentTransaction::class);
|
||||
|
||||
$data = [
|
||||
'provider_id' => $this->getProviderId(),
|
||||
'transaction_id' => $transactionId,
|
||||
'status' => $status,
|
||||
'amount' => $amount,
|
||||
'currency' => $currency,
|
||||
'sale_id' => $saleId,
|
||||
'metadata' => json_encode($metadata),
|
||||
];
|
||||
|
||||
return $transactionModel->insert($data) !== false;
|
||||
}
|
||||
|
||||
protected function updateTransactionStatus(string $transactionId, string $status, array $metadata = []): bool
|
||||
{
|
||||
$transactionModel = model(\App\Models\PaymentTransaction::class);
|
||||
$transaction = $transactionModel
|
||||
->where('transaction_id', $transactionId)
|
||||
->where('provider_id', $this->getProviderId())
|
||||
->first();
|
||||
|
||||
if (!$transaction) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$updateData = ['status' => $status];
|
||||
if (!empty($metadata)) {
|
||||
$existingMetadata = json_decode($transaction['metadata'] ?? '{}', true);
|
||||
$updateData['metadata'] = json_encode(array_merge($existingMetadata, $metadata));
|
||||
}
|
||||
|
||||
return $transactionModel->update($transaction['id'], $updateData);
|
||||
}
|
||||
|
||||
protected function getTransaction(string $transactionId): ?array
|
||||
{
|
||||
$transactionModel = model(\App\Models\PaymentTransaction::class);
|
||||
return $transactionModel
|
||||
->where('transaction_id', $transactionId)
|
||||
->where('provider_id', $this->getProviderId())
|
||||
->first();
|
||||
}
|
||||
|
||||
protected function getSetting(string $key, mixed $default = null): mixed
|
||||
{
|
||||
$settings = $this->getSettings();
|
||||
return $settings[$key] ?? $default;
|
||||
}
|
||||
|
||||
protected function fire(string $event, array $data = []): void
|
||||
{
|
||||
Events::trigger("payment_{$event}", array_merge($data, ['provider' => $this->getProviderId()]));
|
||||
}
|
||||
}
|
||||
36
app/Libraries/Payments/PaymentProviderInterface.php
Normal file
36
app/Libraries/Payments/PaymentProviderInterface.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Libraries\Payments;
|
||||
|
||||
interface PaymentProviderInterface
|
||||
{
|
||||
public function getProviderId(): string;
|
||||
|
||||
public function getProviderName(): string;
|
||||
|
||||
public function getProviderDescription(): string;
|
||||
|
||||
public function getVersion(): string;
|
||||
|
||||
public function getPaymentTypes(): array;
|
||||
|
||||
public function getIcon(?string $paymentType = null): ?string;
|
||||
|
||||
public function initiatePayment(float $amount, string $currency, array $options = []): array;
|
||||
|
||||
public function processCallback(array $data): array;
|
||||
|
||||
public function getPaymentStatus(string $transactionId): array;
|
||||
|
||||
public function refund(string $transactionId, float $amount, string $reason = ''): array;
|
||||
|
||||
public function cancel(string $transactionId): array;
|
||||
|
||||
public function isAvailable(): bool;
|
||||
|
||||
public function getSettings(): array;
|
||||
|
||||
public function saveSettings(array $settings): bool;
|
||||
|
||||
public function getConfigView(): ?string;
|
||||
}
|
||||
101
app/Libraries/Payments/PaymentProviderRegistry.php
Normal file
101
app/Libraries/Payments/PaymentProviderRegistry.php
Normal file
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
namespace App\Libraries\Payments;
|
||||
|
||||
class PaymentProviderRegistry
|
||||
{
|
||||
private static ?PaymentProviderRegistry $instance = null;
|
||||
private array $providers = [];
|
||||
private bool $initialized = false;
|
||||
|
||||
private function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
public static function getInstance(): PaymentProviderRegistry
|
||||
{
|
||||
if (self::$instance === null) {
|
||||
self::$instance = new PaymentProviderRegistry();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public function register(PaymentProviderInterface $provider): void
|
||||
{
|
||||
$providerId = $provider->getProviderId();
|
||||
if (!isset($this->providers[$providerId])) {
|
||||
$this->providers[$providerId] = $provider;
|
||||
}
|
||||
}
|
||||
|
||||
public function unregister(string $providerId): void
|
||||
{
|
||||
unset($this->providers[$providerId]);
|
||||
}
|
||||
|
||||
public function getProvider(string $providerId): ?PaymentProviderInterface
|
||||
{
|
||||
return $this->providers[$providerId] ?? null;
|
||||
}
|
||||
|
||||
public function getProviders(): array
|
||||
{
|
||||
return $this->providers;
|
||||
}
|
||||
|
||||
public function getEnabledProviders(): array
|
||||
{
|
||||
$enabled = [];
|
||||
foreach ($this->providers as $provider) {
|
||||
if ($provider->isAvailable()) {
|
||||
$enabled[$provider->getProviderId()] = $provider;
|
||||
}
|
||||
}
|
||||
return $enabled;
|
||||
}
|
||||
|
||||
public function getEnabledPaymentTypes(): array
|
||||
{
|
||||
$paymentTypes = [];
|
||||
foreach ($this->getEnabledProviders() as $provider) {
|
||||
$providerTypes = $provider->getPaymentTypes();
|
||||
foreach ($providerTypes as $key => $label) {
|
||||
$paymentTypes[$key] = $label;
|
||||
}
|
||||
}
|
||||
return $paymentTypes;
|
||||
}
|
||||
|
||||
public function getProviderForPaymentType(string $paymentTypeKey): ?PaymentProviderInterface
|
||||
{
|
||||
foreach ($this->providers as $provider) {
|
||||
$types = $provider->getPaymentTypes();
|
||||
if (isset($types[$paymentTypeKey])) {
|
||||
return $provider;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getProviderByTransactionId(string $transactionId): ?PaymentProviderInterface
|
||||
{
|
||||
$transactionModel = model(\App\Models\PaymentTransaction::class);
|
||||
$transaction = $transactionModel->where('transaction_id', $transactionId)->first();
|
||||
|
||||
if (!$transaction) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->getProvider($transaction['provider_id']);
|
||||
}
|
||||
|
||||
public function hasProviders(): bool
|
||||
{
|
||||
return !empty($this->providers);
|
||||
}
|
||||
|
||||
public function count(): int
|
||||
{
|
||||
return count($this->providers);
|
||||
}
|
||||
}
|
||||
177
app/Libraries/Payments/Providers/PayPalProvider.php
Normal file
177
app/Libraries/Payments/Providers/PayPalProvider.php
Normal file
@@ -0,0 +1,177 @@
|
||||
<?php
|
||||
|
||||
namespace App\Libraries\Payments\Providers;
|
||||
|
||||
use App\Libraries\Payments\PaymentProviderBase;
|
||||
|
||||
class PayPalProvider extends PaymentProviderBase
|
||||
{
|
||||
public function getProviderId(): string
|
||||
{
|
||||
return 'paypal';
|
||||
}
|
||||
|
||||
public function getProviderName(): string
|
||||
{
|
||||
return 'PayPal';
|
||||
}
|
||||
|
||||
public function getProviderDescription(): string
|
||||
{
|
||||
return 'Accept payments using PayPal (Zettle) card reader terminals and QR code payments.';
|
||||
}
|
||||
|
||||
public function getVersion(): string
|
||||
{
|
||||
return '1.0.0';
|
||||
}
|
||||
|
||||
public function getPaymentTypes(): array
|
||||
{
|
||||
return [
|
||||
'paypal_card' => lang('Sales.paypal_card') ?? 'Card (PayPal/Zettle)',
|
||||
'paypal_qr' => lang('Sales.paypal_qr') ?? 'PayPal QR',
|
||||
];
|
||||
}
|
||||
|
||||
public function getIcon(?string $paymentType = null): ?string
|
||||
{
|
||||
return base_url('images/payment_providers/paypal.svg');
|
||||
}
|
||||
|
||||
public function initiatePayment(float $amount, string $currency = 'USD', array $options = []): array
|
||||
{
|
||||
$orderId = $options['order_id'] ?? uniqid('paypal_', true);
|
||||
$paymentType = $options['payment_type'] ?? 'paypal_card';
|
||||
|
||||
$this->logTransaction(
|
||||
$orderId,
|
||||
'pending',
|
||||
$amount,
|
||||
$currency,
|
||||
$options['sale_id'] ?? null,
|
||||
['payment_type' => $paymentType, 'options' => $options]
|
||||
);
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'transaction_id' => $orderId,
|
||||
'status' => 'pending',
|
||||
'order_id' => $orderId,
|
||||
'amount' => $amount,
|
||||
'currency' => $currency,
|
||||
'payment_type' => $paymentType,
|
||||
'message' => 'Payment initiated. ' .
|
||||
($paymentType === 'paypal_qr'
|
||||
? 'Customer can scan QR code to complete payment.'
|
||||
: 'Use PayPal Zettle terminal to complete.'),
|
||||
];
|
||||
}
|
||||
|
||||
public function processCallback(array $data): array
|
||||
{
|
||||
$eventType = $data['event_type'] ?? '';
|
||||
$orderId = $data['resource']['id'] ?? $data['order_id'] ?? null;
|
||||
|
||||
if (!$orderId) {
|
||||
return ['success' => false, 'error' => 'Missing order ID'];
|
||||
}
|
||||
|
||||
$transaction = $this->getTransaction($orderId);
|
||||
if (!$transaction) {
|
||||
return ['success' => false, 'error' => 'Transaction not found'];
|
||||
}
|
||||
|
||||
switch ($eventType) {
|
||||
case 'CHECKOUT.ORDER.APPROVED':
|
||||
case 'PAYMENT.CAPTURE.COMPLETED':
|
||||
$this->updateTransactionStatus($orderId, 'completed', $data);
|
||||
$this->fire('completed', [
|
||||
'transaction_id' => $orderId,
|
||||
'sale_id' => $transaction['sale_id'],
|
||||
'amount' => $transaction['amount'],
|
||||
]);
|
||||
return ['success' => true, 'status' => 'completed'];
|
||||
|
||||
case 'PAYMENT.CAPTURE.DENIED':
|
||||
case 'PAYMENT.CAPTURE.DECLINED':
|
||||
$this->updateTransactionStatus($orderId, 'failed', $data);
|
||||
$this->fire('failed', [
|
||||
'transaction_id' => $orderId,
|
||||
'error' => $data['resource']['status_details'] ?? 'Payment declined',
|
||||
]);
|
||||
return ['success' => false, 'status' => 'failed', 'error' => 'Payment declined'];
|
||||
|
||||
default:
|
||||
return ['success' => false, 'error' => "Unknown event type: {$eventType}"];
|
||||
}
|
||||
}
|
||||
|
||||
public function getPaymentStatus(string $transactionId): array
|
||||
{
|
||||
$transaction = $this->getTransaction($transactionId);
|
||||
|
||||
if (!$transaction) {
|
||||
return ['success' => false, 'error' => 'Transaction not found'];
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'transaction_id' => $transactionId,
|
||||
'status' => $transaction['status'],
|
||||
'amount' => (float)$transaction['amount'],
|
||||
'currency' => $transaction['currency'],
|
||||
];
|
||||
}
|
||||
|
||||
public function refund(string $transactionId, float $amount, string $reason = ''): array
|
||||
{
|
||||
$transaction = $this->getTransaction($transactionId);
|
||||
|
||||
if (!$transaction) {
|
||||
return ['success' => false, 'error' => 'Transaction not found'];
|
||||
}
|
||||
|
||||
if ($transaction['status'] !== 'completed') {
|
||||
return ['success' => false, 'error' => 'Transaction cannot be refunded'];
|
||||
}
|
||||
|
||||
$this->updateTransactionStatus($transactionId, 'refunded', [
|
||||
'refund_amount' => $amount,
|
||||
'refund_reason' => $reason,
|
||||
]);
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'transaction_id' => $transactionId,
|
||||
'status' => 'refunded',
|
||||
'refund_amount' => $amount,
|
||||
];
|
||||
}
|
||||
|
||||
public function cancel(string $transactionId): array
|
||||
{
|
||||
$transaction = $this->getTransaction($transactionId);
|
||||
|
||||
if (!$transaction) {
|
||||
return ['success' => false, 'error' => 'Transaction not found'];
|
||||
}
|
||||
|
||||
if ($transaction['status'] !== 'pending') {
|
||||
return ['success' => false, 'error' => 'Transaction cannot be cancelled'];
|
||||
}
|
||||
|
||||
$this->updateTransactionStatus($transactionId, 'cancelled');
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'transaction_id' => $transactionId,
|
||||
'status' => 'cancelled',
|
||||
];
|
||||
}
|
||||
|
||||
public function getConfigView(): ?string
|
||||
{
|
||||
return 'Payments/paypal_config';
|
||||
}
|
||||
}
|
||||
169
app/Libraries/Payments/Providers/SumUpProvider.php
Normal file
169
app/Libraries/Payments/Providers/SumUpProvider.php
Normal file
@@ -0,0 +1,169 @@
|
||||
<?php
|
||||
|
||||
namespace App\Libraries\Payments\Providers;
|
||||
|
||||
use App\Libraries\Payments\PaymentProviderBase;
|
||||
|
||||
class SumUpProvider extends PaymentProviderBase
|
||||
{
|
||||
public function getProviderId(): string
|
||||
{
|
||||
return 'sumup';
|
||||
}
|
||||
|
||||
public function getProviderName(): string
|
||||
{
|
||||
return 'SumUp';
|
||||
}
|
||||
|
||||
public function getProviderDescription(): string
|
||||
{
|
||||
return 'Accept card payments using SumUp card reader terminals.';
|
||||
}
|
||||
|
||||
public function getVersion(): string
|
||||
{
|
||||
return '1.0.0';
|
||||
}
|
||||
|
||||
public function getPaymentTypes(): array
|
||||
{
|
||||
return [
|
||||
'sumup_card' => lang('Sales.sumup_card') ?? 'Card (SumUp)',
|
||||
];
|
||||
}
|
||||
|
||||
public function getIcon(?string $paymentType = null): ?string
|
||||
{
|
||||
return base_url('images/payment_providers/sumup.svg');
|
||||
}
|
||||
|
||||
public function initiatePayment(float $amount, string $currency = 'USD', array $options = []): array
|
||||
{
|
||||
$checkoutReference = $options['checkout_reference'] ?? uniqid('sumup_', true);
|
||||
|
||||
$this->logTransaction(
|
||||
$checkoutReference,
|
||||
'pending',
|
||||
$amount,
|
||||
$currency,
|
||||
$options['sale_id'] ?? null,
|
||||
['options' => $options]
|
||||
);
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'transaction_id' => $checkoutReference,
|
||||
'status' => 'pending',
|
||||
'checkout_reference' => $checkoutReference,
|
||||
'amount' => $amount,
|
||||
'currency' => $currency,
|
||||
'message' => 'Payment initiated. Use SumUp terminal to complete.',
|
||||
];
|
||||
}
|
||||
|
||||
public function processCallback(array $data): array
|
||||
{
|
||||
$eventType = $data['event_type'] ?? '';
|
||||
$checkoutId = $data['checkout_id'] ?? $data['id'] ?? null;
|
||||
|
||||
if (!$checkoutId) {
|
||||
return ['success' => false, 'error' => 'Missing checkout ID'];
|
||||
}
|
||||
|
||||
$transaction = $this->getTransaction($checkoutId);
|
||||
if (!$transaction) {
|
||||
return ['success' => false, 'error' => 'Transaction not found'];
|
||||
}
|
||||
|
||||
switch ($eventType) {
|
||||
case 'payment.success':
|
||||
$this->updateTransactionStatus($checkoutId, 'completed', $data);
|
||||
$this->fire('completed', [
|
||||
'transaction_id' => $checkoutId,
|
||||
'sale_id' => $transaction['sale_id'],
|
||||
'amount' => $transaction['amount'],
|
||||
]);
|
||||
return ['success' => true, 'status' => 'completed'];
|
||||
|
||||
case 'payment.failed':
|
||||
$this->updateTransactionStatus($checkoutId, 'failed', $data);
|
||||
$this->fire('failed', [
|
||||
'transaction_id' => $checkoutId,
|
||||
'error' => $data['error_message'] ?? 'Unknown error',
|
||||
]);
|
||||
return ['success' => false, 'status' => 'failed', 'error' => $data['error_message'] ?? 'Payment failed'];
|
||||
|
||||
default:
|
||||
return ['success' => false, 'error' => "Unknown event type: {$eventType}"];
|
||||
}
|
||||
}
|
||||
|
||||
public function getPaymentStatus(string $transactionId): array
|
||||
{
|
||||
$transaction = $this->getTransaction($transactionId);
|
||||
|
||||
if (!$transaction) {
|
||||
return ['success' => false, 'error' => 'Transaction not found'];
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'transaction_id' => $transactionId,
|
||||
'status' => $transaction['status'],
|
||||
'amount' => (float)$transaction['amount'],
|
||||
'currency' => $transaction['currency'],
|
||||
];
|
||||
}
|
||||
|
||||
public function refund(string $transactionId, float $amount, string $reason = ''): array
|
||||
{
|
||||
$transaction = $this->getTransaction($transactionId);
|
||||
|
||||
if (!$transaction) {
|
||||
return ['success' => false, 'error' => 'Transaction not found'];
|
||||
}
|
||||
|
||||
if ($transaction['status'] !== 'completed') {
|
||||
return ['success' => false, 'error' => 'Transaction cannot be refunded'];
|
||||
}
|
||||
|
||||
$this->updateTransactionStatus($transactionId, 'refunded', [
|
||||
'refund_amount' => $amount,
|
||||
'refund_reason' => $reason,
|
||||
]);
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'transaction_id' => $transactionId,
|
||||
'status' => 'refunded',
|
||||
'refund_amount' => $amount,
|
||||
];
|
||||
}
|
||||
|
||||
public function cancel(string $transactionId): array
|
||||
{
|
||||
$transaction = $this->getTransaction($transactionId);
|
||||
|
||||
if (!$transaction) {
|
||||
return ['success' => false, 'error' => 'Transaction not found'];
|
||||
}
|
||||
|
||||
if ($transaction['status'] !== 'pending') {
|
||||
return ['success' => false, 'error' => 'Transaction cannot be cancelled'];
|
||||
}
|
||||
|
||||
$this->updateTransactionStatus($transactionId, 'cancelled');
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'transaction_id' => $transactionId,
|
||||
'status' => 'cancelled',
|
||||
];
|
||||
}
|
||||
|
||||
public function getConfigView(): ?string
|
||||
{
|
||||
return 'Payments/sumup_config';
|
||||
}
|
||||
}
|
||||
82
app/Models/PaymentTransaction.php
Normal file
82
app/Models/PaymentTransaction.php
Normal file
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class PaymentTransaction extends Model
|
||||
{
|
||||
protected $table = 'payment_transactions';
|
||||
protected $primaryKey = 'id';
|
||||
protected $useAutoIncrement = true;
|
||||
protected $useSoftDeletes = false;
|
||||
protected $allowedFields = [
|
||||
'provider_id',
|
||||
'sale_id',
|
||||
'transaction_id',
|
||||
'amount',
|
||||
'currency',
|
||||
'status',
|
||||
'metadata',
|
||||
'created_at',
|
||||
'updated_at'
|
||||
];
|
||||
protected $useTimestamps = true;
|
||||
protected $createdField = 'created_at';
|
||||
protected $updatedField = 'updated_at';
|
||||
|
||||
public const STATUS_PENDING = 'pending';
|
||||
public const STATUS_AUTHORIZED = 'authorized';
|
||||
public const STATUS_COMPLETED = 'completed';
|
||||
public const STATUS_FAILED = 'failed';
|
||||
public const STATUS_REFUNDED = 'refunded';
|
||||
public const STATUS_CANCELLED = 'cancelled';
|
||||
|
||||
public function getTransaction(string $transactionId, ?string $providerId = null): ?array
|
||||
{
|
||||
$builder = $this->builder();
|
||||
$builder->where('transaction_id', $transactionId);
|
||||
|
||||
if ($providerId !== null) {
|
||||
$builder->where('provider_id', $providerId);
|
||||
}
|
||||
|
||||
$result = $builder->get()->getRowArray();
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function getTransactionsBySale(int $saleId): array
|
||||
{
|
||||
return $this->where('sale_id', $saleId)
|
||||
->orderBy('created_at', 'DESC')
|
||||
->findAll();
|
||||
}
|
||||
|
||||
public function getPendingTransactions(?string $providerId = null): array
|
||||
{
|
||||
$builder = $this->builder();
|
||||
$builder->where('status', self::STATUS_PENDING);
|
||||
|
||||
if ($providerId !== null) {
|
||||
$builder->where('provider_id', $providerId);
|
||||
}
|
||||
|
||||
return $builder->get()->getResultArray();
|
||||
}
|
||||
|
||||
public function updateStatus(int $id, string $status, array $additionalData = []): bool
|
||||
{
|
||||
$data = ['status' => $status];
|
||||
|
||||
if (!empty($additionalData['metadata'])) {
|
||||
$existing = $this->find($id);
|
||||
if ($existing) {
|
||||
$existingMetadata = json_decode($existing['metadata'] ?? '{}', true);
|
||||
$data['metadata'] = json_encode(array_merge($existingMetadata, $additionalData['metadata']));
|
||||
}
|
||||
}
|
||||
|
||||
return $this->update($id, $data);
|
||||
}
|
||||
}
|
||||
@@ -465,6 +465,12 @@ helper('url');
|
||||
<div class="btn btn-sm btn-success pull-right" id="add_payment_button" tabindex="<?= ++$tabindex ?>">
|
||||
<span class="glyphicon glyphicon-credit-card"> </span><?= lang(ucfirst($controller_name) . '.add_payment') ?>
|
||||
</div>
|
||||
|
||||
<?php if (function_exists('payment_provider_content')): ?>
|
||||
<div id="payment_provider_actions">
|
||||
<?= payment_provider_content('register_payment_actions', ['amount_due' => $amount_due ?? 0, 'payments' => $payments ?? []]) ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php } ?>
|
||||
|
||||
<?php if (count($payments) > 0) { // Only show this part if there is at least one payment entered. ?>
|
||||
|
||||
Reference in New Issue
Block a user