chore: 补充财务与支付模块源码
Some checks failed
Tests / PHP 8.1 (push) Has been cancelled
Tests / PHP 8.2 (push) Has been cancelled
Tests / PHP 8.3 (push) Has been cancelled
Some checks failed
Tests / PHP 8.1 (push) Has been cancelled
Tests / PHP 8.2 (push) Has been cancelled
Tests / PHP 8.3 (push) Has been cancelled
This commit is contained in:
parent
9595267ee7
commit
2b6c4030e5
6
.gitignore
vendored
6
.gitignore
vendored
@ -94,5 +94,11 @@ _ide_helper_models.php
|
||||
.phpunit.result.cache
|
||||
modules/*
|
||||
!modules/.gitkeep
|
||||
!modules/EcommerceFinance/
|
||||
!modules/EcommerceFinance/**
|
||||
!modules/OfflinePayments/
|
||||
!modules/OfflinePayments/**
|
||||
!modules/PaypalStandard/
|
||||
!modules/PaypalStandard/**
|
||||
.laravelstatsrc
|
||||
.DS_Store
|
||||
|
||||
@ -0,0 +1,231 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('ec_channels', function (Blueprint $table) {
|
||||
$table->increments('id');
|
||||
$table->unsignedInteger('company_id');
|
||||
$table->string('code', 64);
|
||||
$table->string('name');
|
||||
$table->string('type', 32)->default('marketplace');
|
||||
$table->boolean('enabled')->default(true);
|
||||
$table->unsignedInteger('created_by')->nullable();
|
||||
$table->string('created_from', 100)->nullable();
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
|
||||
$table->unique(['company_id', 'code', 'deleted_at']);
|
||||
});
|
||||
|
||||
Schema::create('ec_stores', function (Blueprint $table) {
|
||||
$table->increments('id');
|
||||
$table->unsignedInteger('company_id');
|
||||
$table->unsignedInteger('channel_id');
|
||||
$table->string('name');
|
||||
$table->string('external_id', 128);
|
||||
$table->unsignedInteger('settlement_account_id')->nullable();
|
||||
$table->string('timezone', 64)->default('Asia/Shanghai');
|
||||
$table->string('currency_code', 3)->default('CNY');
|
||||
$table->boolean('enabled')->default(true);
|
||||
$table->unsignedInteger('created_by')->nullable();
|
||||
$table->string('created_from', 100)->nullable();
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
|
||||
$table->index(['company_id', 'channel_id']);
|
||||
$table->unique(['company_id', 'channel_id', 'external_id', 'deleted_at']);
|
||||
});
|
||||
|
||||
Schema::create('ec_sku_mappings', function (Blueprint $table) {
|
||||
$table->increments('id');
|
||||
$table->unsignedInteger('company_id');
|
||||
$table->unsignedInteger('store_id');
|
||||
$table->unsignedInteger('item_id')->nullable();
|
||||
$table->string('external_sku_id', 128);
|
||||
$table->string('external_sku_code', 128)->nullable();
|
||||
$table->string('external_sku_name')->nullable();
|
||||
$table->unsignedInteger('created_by')->nullable();
|
||||
$table->string('created_from', 100)->nullable();
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
|
||||
$table->unique(['store_id', 'external_sku_id', 'deleted_at']);
|
||||
$table->index(['company_id', 'item_id']);
|
||||
});
|
||||
|
||||
Schema::create('ec_import_batches', function (Blueprint $table) {
|
||||
$table->increments('id');
|
||||
$table->unsignedInteger('company_id');
|
||||
$table->unsignedInteger('store_id')->nullable();
|
||||
$table->string('source_type', 32);
|
||||
$table->string('source_file');
|
||||
$table->string('checksum', 64);
|
||||
$table->string('status', 32)->default('queued');
|
||||
$table->unsignedInteger('row_count')->default(0);
|
||||
$table->unsignedInteger('success_count')->default(0);
|
||||
$table->unsignedInteger('error_count')->default(0);
|
||||
$table->json('summary')->nullable();
|
||||
$table->text('error_message')->nullable();
|
||||
$table->timestamp('started_at')->nullable();
|
||||
$table->timestamp('completed_at')->nullable();
|
||||
$table->unsignedInteger('created_by')->nullable();
|
||||
$table->string('created_from', 100)->nullable();
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
|
||||
$table->unique(['company_id', 'source_type', 'checksum']);
|
||||
$table->index(['company_id', 'status']);
|
||||
});
|
||||
|
||||
Schema::create('ec_orders', function (Blueprint $table) {
|
||||
$table->increments('id');
|
||||
$table->unsignedInteger('company_id');
|
||||
$table->unsignedInteger('store_id');
|
||||
$table->string('external_order_id', 128);
|
||||
$table->string('order_status', 32);
|
||||
$table->dateTime('ordered_at')->nullable();
|
||||
$table->dateTime('paid_at')->nullable();
|
||||
$table->dateTime('shipped_at')->nullable();
|
||||
$table->dateTime('completed_at')->nullable();
|
||||
$table->string('currency_code', 3)->default('CNY');
|
||||
$table->decimal('gross_amount', 20, 6)->default(0);
|
||||
$table->decimal('buyer_paid_amount', 20, 6)->default(0);
|
||||
$table->decimal('platform_subsidy_amount', 20, 6)->default(0);
|
||||
$table->decimal('merchant_subsidy_amount', 20, 6)->default(0);
|
||||
$table->decimal('refunded_amount', 20, 6)->default(0);
|
||||
$table->json('payload')->nullable();
|
||||
$table->unsignedInteger('created_by')->nullable();
|
||||
$table->string('created_from', 100)->nullable();
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
|
||||
$table->unique(['store_id', 'external_order_id']);
|
||||
$table->index(['company_id', 'order_status']);
|
||||
$table->index(['company_id', 'completed_at']);
|
||||
});
|
||||
|
||||
Schema::create('ec_order_items', function (Blueprint $table) {
|
||||
$table->increments('id');
|
||||
$table->unsignedInteger('company_id');
|
||||
$table->unsignedInteger('order_id');
|
||||
$table->unsignedInteger('item_id')->nullable();
|
||||
$table->string('external_line_id', 128);
|
||||
$table->string('sku_code', 128)->nullable();
|
||||
$table->string('name');
|
||||
$table->decimal('quantity', 20, 6);
|
||||
$table->decimal('sale_amount', 20, 6)->default(0);
|
||||
$table->decimal('discount_amount', 20, 6)->default(0);
|
||||
$table->decimal('refund_amount', 20, 6)->default(0);
|
||||
$table->decimal('cost_amount', 20, 6)->default(0);
|
||||
$table->unsignedInteger('created_by')->nullable();
|
||||
$table->string('created_from', 100)->nullable();
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
|
||||
$table->unique(['order_id', 'external_line_id']);
|
||||
$table->index(['company_id', 'item_id']);
|
||||
});
|
||||
|
||||
Schema::create('ec_order_events', function (Blueprint $table) {
|
||||
$table->increments('id');
|
||||
$table->unsignedInteger('company_id');
|
||||
$table->unsignedInteger('order_id');
|
||||
$table->string('event_type', 32);
|
||||
$table->string('external_event_id', 128);
|
||||
$table->dateTime('event_at');
|
||||
$table->json('payload')->nullable();
|
||||
$table->unsignedInteger('created_by')->nullable();
|
||||
$table->string('created_from', 100)->nullable();
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
|
||||
$table->unique(['order_id', 'event_type', 'external_event_id']);
|
||||
$table->index(['company_id', 'event_at']);
|
||||
});
|
||||
|
||||
Schema::create('ec_settlements', function (Blueprint $table) {
|
||||
$table->increments('id');
|
||||
$table->unsignedInteger('company_id');
|
||||
$table->unsignedInteger('store_id');
|
||||
$table->unsignedInteger('transaction_id')->nullable();
|
||||
$table->string('external_settlement_id', 128);
|
||||
$table->dateTime('period_start')->nullable();
|
||||
$table->dateTime('period_end')->nullable();
|
||||
$table->dateTime('settled_at')->nullable();
|
||||
$table->string('currency_code', 3)->default('CNY');
|
||||
$table->decimal('gross_amount', 20, 6)->default(0);
|
||||
$table->decimal('fee_amount', 20, 6)->default(0);
|
||||
$table->decimal('refund_amount', 20, 6)->default(0);
|
||||
$table->decimal('net_amount', 20, 6)->default(0);
|
||||
$table->string('status', 32)->default('imported');
|
||||
$table->json('payload')->nullable();
|
||||
$table->unsignedInteger('created_by')->nullable();
|
||||
$table->string('created_from', 100)->nullable();
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
|
||||
$table->unique(['store_id', 'external_settlement_id']);
|
||||
$table->index(['company_id', 'status']);
|
||||
$table->index(['company_id', 'settled_at']);
|
||||
});
|
||||
|
||||
Schema::create('ec_settlement_lines', function (Blueprint $table) {
|
||||
$table->increments('id');
|
||||
$table->unsignedInteger('company_id');
|
||||
$table->unsignedInteger('settlement_id');
|
||||
$table->string('line_type', 32);
|
||||
$table->string('external_line_id', 128);
|
||||
$table->string('external_order_id', 128)->nullable();
|
||||
$table->decimal('amount', 20, 6);
|
||||
$table->dateTime('occurred_at')->nullable();
|
||||
$table->json('payload')->nullable();
|
||||
$table->unsignedInteger('created_by')->nullable();
|
||||
$table->string('created_from', 100)->nullable();
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
|
||||
$table->unique(['settlement_id', 'external_line_id']);
|
||||
$table->index(['company_id', 'external_order_id']);
|
||||
});
|
||||
|
||||
Schema::create('ec_reconciliation_matches', function (Blueprint $table) {
|
||||
$table->increments('id');
|
||||
$table->unsignedInteger('company_id');
|
||||
$table->unsignedInteger('settlement_line_id');
|
||||
$table->unsignedInteger('order_id')->nullable();
|
||||
$table->string('match_type', 32);
|
||||
$table->decimal('matched_amount', 20, 6)->default(0);
|
||||
$table->string('status', 32)->default('unmatched');
|
||||
$table->string('rule_version', 32)->nullable();
|
||||
$table->unsignedInteger('confirmed_by')->nullable();
|
||||
$table->timestamp('confirmed_at')->nullable();
|
||||
$table->text('note')->nullable();
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
|
||||
$table->unique(['settlement_line_id', 'order_id']);
|
||||
$table->index(['company_id', 'status']);
|
||||
$table->index(['company_id', 'order_id']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('ec_reconciliation_matches');
|
||||
Schema::dropIfExists('ec_settlement_lines');
|
||||
Schema::dropIfExists('ec_settlements');
|
||||
Schema::dropIfExists('ec_order_events');
|
||||
Schema::dropIfExists('ec_order_items');
|
||||
Schema::dropIfExists('ec_orders');
|
||||
Schema::dropIfExists('ec_import_batches');
|
||||
Schema::dropIfExists('ec_sku_mappings');
|
||||
Schema::dropIfExists('ec_stores');
|
||||
Schema::dropIfExists('ec_channels');
|
||||
}
|
||||
};
|
||||
50
modules/EcommerceFinance/Http/Controllers/Api/Channels.php
Normal file
50
modules/EcommerceFinance/Http/Controllers/Api/Channels.php
Normal file
@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\EcommerceFinance\Http\Controllers\Api;
|
||||
|
||||
use App\Abstracts\Http\ApiController;
|
||||
use Modules\EcommerceFinance\Http\Requests\Channel as Request;
|
||||
use Modules\EcommerceFinance\Models\Channel;
|
||||
|
||||
class Channels extends ApiController
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
return response()->json(Channel::query()->orderBy('name')->paginate($this->limit()));
|
||||
}
|
||||
|
||||
public function show(Channel $channel)
|
||||
{
|
||||
return response()->json($channel);
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
$channel = Channel::create($this->attributes($request));
|
||||
|
||||
return response()
|
||||
->json($channel, 201)
|
||||
->header('Location', route('api.ecommerce-finance.channels.show', $channel));
|
||||
}
|
||||
|
||||
public function update(Channel $channel, Request $request)
|
||||
{
|
||||
$channel->update($request->validated());
|
||||
|
||||
return response()->json($channel->fresh());
|
||||
}
|
||||
|
||||
private function attributes(Request $request): array
|
||||
{
|
||||
return array_merge($request->validated(), [
|
||||
'company_id' => company_id(),
|
||||
'created_by' => user_id(),
|
||||
'created_from' => 'ecommerce-finance',
|
||||
]);
|
||||
}
|
||||
|
||||
private function limit(): int
|
||||
{
|
||||
return min(max((int) request('limit', 25), 1), 100);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\EcommerceFinance\Http\Controllers\Api;
|
||||
|
||||
use App\Abstracts\Http\ApiController;
|
||||
use Illuminate\Database\QueryException;
|
||||
use Modules\EcommerceFinance\Http\Requests\ImportBatch as Request;
|
||||
use Modules\EcommerceFinance\Jobs\ProcessImportBatch;
|
||||
use Modules\EcommerceFinance\Models\ImportBatch;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class ImportBatches extends ApiController
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$batches = ImportBatch::with('store.channel')
|
||||
->latest('id')
|
||||
->paginate($this->limit());
|
||||
|
||||
return response()->json($batches);
|
||||
}
|
||||
|
||||
public function show(ImportBatch $importBatch)
|
||||
{
|
||||
return response()->json($importBatch->load('store.channel'));
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
if (! Storage::disk('uploads')->exists($request->input('source_file'))) {
|
||||
return $this->errorBadRequest('The source file does not exist in the private uploads storage.');
|
||||
}
|
||||
|
||||
try {
|
||||
$batch = ImportBatch::create(array_merge($request->validated(), [
|
||||
'company_id' => company_id(),
|
||||
'status' => 'queued',
|
||||
'created_by' => user_id(),
|
||||
'created_from' => 'ecommerce-finance',
|
||||
]));
|
||||
} catch (QueryException $exception) {
|
||||
return $this->errorBadRequest('An identical import batch has already been created.');
|
||||
}
|
||||
|
||||
$this->dispatch(new ProcessImportBatch($batch->id));
|
||||
|
||||
$batch->refresh();
|
||||
|
||||
return response()
|
||||
->json($batch, 201)
|
||||
->header('Location', route('api.ecommerce-finance.import-batches.show', $batch));
|
||||
}
|
||||
|
||||
private function limit(): int
|
||||
{
|
||||
return min(max((int) request('limit', 25), 1), 100);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\EcommerceFinance\Http\Controllers\Api;
|
||||
|
||||
use App\Abstracts\Http\ApiController;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
use Modules\EcommerceFinance\Http\Requests\ImportFile as Request;
|
||||
|
||||
class ImportFiles extends ApiController
|
||||
{
|
||||
public function store(Request $request)
|
||||
{
|
||||
$file = $request->file('source_file');
|
||||
$checksum = hash_file('sha256', $file->getRealPath());
|
||||
$extension = strtolower($file->getClientOriginalExtension() ?: 'csv');
|
||||
$path = 'ecommerce-finance/' . company_id() . '/' . Str::uuid() . '.' . $extension;
|
||||
|
||||
Storage::disk('uploads')->putFileAs(
|
||||
dirname($path),
|
||||
$file,
|
||||
basename($path)
|
||||
);
|
||||
|
||||
return response()->json([
|
||||
'source_file' => $path,
|
||||
'checksum' => $checksum,
|
||||
], 201);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\EcommerceFinance\Http\Controllers\Api;
|
||||
|
||||
use App\Abstracts\Http\ApiController;
|
||||
use Modules\EcommerceFinance\Models\ReconciliationMatch;
|
||||
|
||||
class ReconciliationExceptions extends ApiController
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$matches = ReconciliationMatch::with('settlementLine.settlement.store', 'order')
|
||||
->whereIn('status', ['unmatched', 'partial_matched'])
|
||||
->latest('id')
|
||||
->paginate(min(max((int) request('limit', 25), 1), 100));
|
||||
|
||||
return response()->json($matches);
|
||||
}
|
||||
}
|
||||
54
modules/EcommerceFinance/Http/Controllers/Api/Stores.php
Normal file
54
modules/EcommerceFinance/Http/Controllers/Api/Stores.php
Normal file
@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\EcommerceFinance\Http\Controllers\Api;
|
||||
|
||||
use App\Abstracts\Http\ApiController;
|
||||
use Modules\EcommerceFinance\Http\Requests\Store as Request;
|
||||
use Modules\EcommerceFinance\Models\Store;
|
||||
|
||||
class Stores extends ApiController
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$stores = Store::with('channel', 'settlementAccount')
|
||||
->orderBy('name')
|
||||
->paginate($this->limit());
|
||||
|
||||
return response()->json($stores);
|
||||
}
|
||||
|
||||
public function show(Store $store)
|
||||
{
|
||||
return response()->json($store->load('channel', 'settlementAccount'));
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
$store = Store::create($this->attributes($request));
|
||||
|
||||
return response()
|
||||
->json($store, 201)
|
||||
->header('Location', route('api.ecommerce-finance.stores.show', $store));
|
||||
}
|
||||
|
||||
public function update(Store $store, Request $request)
|
||||
{
|
||||
$store->update($request->validated());
|
||||
|
||||
return response()->json($store->fresh()->load('channel', 'settlementAccount'));
|
||||
}
|
||||
|
||||
private function attributes(Request $request): array
|
||||
{
|
||||
return array_merge($request->validated(), [
|
||||
'company_id' => company_id(),
|
||||
'created_by' => user_id(),
|
||||
'created_from' => 'ecommerce-finance',
|
||||
]);
|
||||
}
|
||||
|
||||
private function limit(): int
|
||||
{
|
||||
return min(max((int) request('limit', 25), 1), 100);
|
||||
}
|
||||
}
|
||||
31
modules/EcommerceFinance/Http/Requests/Channel.php
Normal file
31
modules/EcommerceFinance/Http/Requests/Channel.php
Normal file
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\EcommerceFinance\Http\Requests;
|
||||
|
||||
use App\Abstracts\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class Channel extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'code' => [
|
||||
'required',
|
||||
'string',
|
||||
'max:64',
|
||||
Rule::unique('ec_channels', 'code')
|
||||
->where('company_id', company_id())
|
||||
->ignore($this->route('channel')),
|
||||
],
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'type' => ['required', 'string', 'max:32'],
|
||||
'enabled' => ['sometimes', 'boolean'],
|
||||
];
|
||||
}
|
||||
}
|
||||
24
modules/EcommerceFinance/Http/Requests/ImportBatch.php
Normal file
24
modules/EcommerceFinance/Http/Requests/ImportBatch.php
Normal file
@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\EcommerceFinance\Http\Requests;
|
||||
|
||||
use App\Abstracts\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class ImportBatch extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'store_id' => ['required', 'integer', Rule::exists('ec_stores', 'id')->where('company_id', company_id())],
|
||||
'source_type' => ['required', Rule::in(['orders', 'settlements'])],
|
||||
'source_file' => ['required', 'string', 'max:255'],
|
||||
'checksum' => ['required', 'string', 'size:64', 'regex:/^[a-f0-9]{64}$/i'],
|
||||
];
|
||||
}
|
||||
}
|
||||
20
modules/EcommerceFinance/Http/Requests/ImportFile.php
Normal file
20
modules/EcommerceFinance/Http/Requests/ImportFile.php
Normal file
@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\EcommerceFinance\Http\Requests;
|
||||
|
||||
use App\Abstracts\Http\FormRequest;
|
||||
|
||||
class ImportFile extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'source_file' => ['required', 'file', 'mimes:csv,txt', 'max:10240'],
|
||||
];
|
||||
}
|
||||
}
|
||||
35
modules/EcommerceFinance/Http/Requests/Store.php
Normal file
35
modules/EcommerceFinance/Http/Requests/Store.php
Normal file
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\EcommerceFinance\Http\Requests;
|
||||
|
||||
use App\Abstracts\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class Store extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'channel_id' => ['required', 'integer', Rule::exists('ec_channels', 'id')->where('company_id', company_id())],
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'external_id' => [
|
||||
'required',
|
||||
'string',
|
||||
'max:128',
|
||||
Rule::unique('ec_stores', 'external_id')
|
||||
->where('company_id', company_id())
|
||||
->where('channel_id', $this->input('channel_id'))
|
||||
->ignore($this->route('store')),
|
||||
],
|
||||
'settlement_account_id' => ['nullable', 'integer', Rule::exists('accounts', 'id')->where('company_id', company_id())],
|
||||
'timezone' => ['sometimes', 'timezone'],
|
||||
'currency_code' => ['sometimes', 'string', 'size:3'],
|
||||
'enabled' => ['sometimes', 'boolean'],
|
||||
];
|
||||
}
|
||||
}
|
||||
73
modules/EcommerceFinance/Jobs/MatchSettlementLines.php
Normal file
73
modules/EcommerceFinance/Jobs/MatchSettlementLines.php
Normal file
@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\EcommerceFinance\Jobs;
|
||||
|
||||
use App\Abstracts\JobShouldQueue;
|
||||
use Modules\EcommerceFinance\Models\Order;
|
||||
use Modules\EcommerceFinance\Models\ReconciliationMatch;
|
||||
use Modules\EcommerceFinance\Models\Settlement;
|
||||
use Modules\EcommerceFinance\Models\SettlementLine;
|
||||
|
||||
class MatchSettlementLines extends JobShouldQueue
|
||||
{
|
||||
public function __construct(private readonly int $settlementId)
|
||||
{
|
||||
}
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
$settlement = Settlement::withoutGlobalScopes()->findOrFail($this->settlementId);
|
||||
$lines = SettlementLine::withoutGlobalScopes()->where('settlement_id', $settlement->id)->get();
|
||||
|
||||
foreach ($lines as $line) {
|
||||
$order = null;
|
||||
$status = 'ignored';
|
||||
$matchedAmount = '0';
|
||||
|
||||
if (! empty($line->external_order_id)) {
|
||||
$order = Order::withoutGlobalScopes()
|
||||
->where('store_id', $settlement->store_id)
|
||||
->where('external_order_id', $line->external_order_id)
|
||||
->first();
|
||||
|
||||
if (! $order) {
|
||||
$status = 'unmatched';
|
||||
} else {
|
||||
$matchedAmount = $line->amount;
|
||||
$status = $this->amountMatches($line->amount, $this->expectedAmount($order, $line->line_type))
|
||||
? 'auto_matched'
|
||||
: 'partial_matched';
|
||||
}
|
||||
}
|
||||
|
||||
ReconciliationMatch::withoutGlobalScopes()->updateOrCreate(
|
||||
['settlement_line_id' => $line->id, 'order_id' => $order?->id],
|
||||
[
|
||||
'company_id' => $settlement->company_id,
|
||||
'match_type' => $status === 'auto_matched' ? 'rule' : 'exception',
|
||||
'matched_amount' => $matchedAmount,
|
||||
'status' => $status,
|
||||
'rule_version' => '1.0',
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private function expectedAmount(Order $order, string $lineType): string
|
||||
{
|
||||
if ($lineType === 'refund') {
|
||||
return $order->refunded_amount;
|
||||
}
|
||||
|
||||
return bcsub(
|
||||
bcadd($order->buyer_paid_amount, $order->platform_subsidy_amount, 6),
|
||||
$order->refunded_amount,
|
||||
6
|
||||
);
|
||||
}
|
||||
|
||||
private function amountMatches(string $actual, string $expected): bool
|
||||
{
|
||||
return bccomp(ltrim($actual, '-'), ltrim($expected, '-'), 6) === 0;
|
||||
}
|
||||
}
|
||||
295
modules/EcommerceFinance/Jobs/ProcessImportBatch.php
Normal file
295
modules/EcommerceFinance/Jobs/ProcessImportBatch.php
Normal file
@ -0,0 +1,295 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\EcommerceFinance\Jobs;
|
||||
|
||||
use App\Abstracts\JobShouldQueue;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Modules\EcommerceFinance\Models\ImportBatch;
|
||||
use Modules\EcommerceFinance\Models\Order;
|
||||
use Modules\EcommerceFinance\Models\OrderItem;
|
||||
use Modules\EcommerceFinance\Models\Settlement;
|
||||
use Modules\EcommerceFinance\Models\SettlementLine;
|
||||
use RuntimeException;
|
||||
use Throwable;
|
||||
|
||||
class ProcessImportBatch extends JobShouldQueue
|
||||
{
|
||||
public function __construct(private readonly int $batchId)
|
||||
{
|
||||
}
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
$batch = ImportBatch::withoutGlobalScopes()->findOrFail($this->batchId);
|
||||
|
||||
if (in_array($batch->status, ['completed', 'completed_with_errors'], true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$batch->update([
|
||||
'status' => 'processing',
|
||||
'started_at' => now(),
|
||||
'error_message' => null,
|
||||
]);
|
||||
|
||||
try {
|
||||
$summary = $batch->source_type === 'orders'
|
||||
? $this->importOrders($batch)
|
||||
: $this->importSettlements($batch);
|
||||
|
||||
$batch->update([
|
||||
'status' => empty($summary['errors']) ? 'completed' : 'completed_with_errors',
|
||||
'row_count' => $summary['rows'],
|
||||
'success_count' => $summary['success'],
|
||||
'error_count' => count($summary['errors']),
|
||||
'summary' => $summary,
|
||||
'completed_at' => now(),
|
||||
]);
|
||||
|
||||
foreach ($summary['settlement_ids'] ?? [] as $settlementId) {
|
||||
$this->dispatch(new MatchSettlementLines($settlementId));
|
||||
}
|
||||
} catch (Throwable $exception) {
|
||||
$batch->update([
|
||||
'status' => 'failed',
|
||||
'error_message' => $exception->getMessage(),
|
||||
'completed_at' => now(),
|
||||
]);
|
||||
|
||||
throw $exception;
|
||||
}
|
||||
}
|
||||
|
||||
private function importOrders(ImportBatch $batch): array
|
||||
{
|
||||
$summary = ['rows' => 0, 'success' => 0, 'errors' => []];
|
||||
|
||||
foreach ($this->rows($batch, ['external_order_id', 'external_line_id', 'order_status', 'item_name', 'quantity']) as $line => $row) {
|
||||
++$summary['rows'];
|
||||
|
||||
try {
|
||||
DB::transaction(function () use ($batch, $row) {
|
||||
$order = Order::withoutGlobalScopes()->withTrashed()->updateOrCreate(
|
||||
['store_id' => $batch->store_id, 'external_order_id' => $this->required($row, 'external_order_id')],
|
||||
[
|
||||
'company_id' => $batch->company_id,
|
||||
'order_status' => $this->required($row, 'order_status'),
|
||||
'ordered_at' => $this->date($row, 'ordered_at'),
|
||||
'paid_at' => $this->date($row, 'paid_at'),
|
||||
'shipped_at' => $this->date($row, 'shipped_at'),
|
||||
'completed_at' => $this->date($row, 'completed_at'),
|
||||
'currency_code' => strtoupper($this->value($row, 'currency_code') ?: 'CNY'),
|
||||
'gross_amount' => $this->money($row, 'gross_amount'),
|
||||
'buyer_paid_amount' => $this->money($row, 'buyer_paid_amount'),
|
||||
'platform_subsidy_amount' => $this->money($row, 'platform_subsidy_amount'),
|
||||
'merchant_subsidy_amount' => $this->money($row, 'merchant_subsidy_amount'),
|
||||
'refunded_amount' => $this->money($row, 'refunded_amount'),
|
||||
'payload' => $row,
|
||||
'created_by' => $batch->created_by,
|
||||
'created_from' => 'ecommerce-finance:import',
|
||||
]
|
||||
);
|
||||
|
||||
if ($order->trashed()) {
|
||||
$order->restore();
|
||||
}
|
||||
|
||||
$item = OrderItem::withoutGlobalScopes()->withTrashed()->updateOrCreate(
|
||||
['order_id' => $order->id, 'external_line_id' => $this->required($row, 'external_line_id')],
|
||||
[
|
||||
'company_id' => $batch->company_id,
|
||||
'sku_code' => $this->value($row, 'sku_code'),
|
||||
'name' => $this->required($row, 'item_name'),
|
||||
'quantity' => $this->number($row, 'quantity'),
|
||||
'sale_amount' => $this->money($row, 'sale_amount'),
|
||||
'discount_amount' => $this->money($row, 'discount_amount'),
|
||||
'refund_amount' => $this->money($row, 'refund_amount'),
|
||||
'created_by' => $batch->created_by,
|
||||
'created_from' => 'ecommerce-finance:import',
|
||||
]
|
||||
);
|
||||
|
||||
if ($item->trashed()) {
|
||||
$item->restore();
|
||||
}
|
||||
});
|
||||
|
||||
++$summary['success'];
|
||||
} catch (Throwable $exception) {
|
||||
$summary['errors'][] = ['line' => $line, 'message' => $exception->getMessage()];
|
||||
}
|
||||
}
|
||||
|
||||
return $summary;
|
||||
}
|
||||
|
||||
private function importSettlements(ImportBatch $batch): array
|
||||
{
|
||||
$summary = ['rows' => 0, 'success' => 0, 'errors' => [], 'settlement_ids' => []];
|
||||
|
||||
foreach ($this->rows($batch, ['external_settlement_id', 'external_line_id', 'line_type', 'line_amount']) as $line => $row) {
|
||||
++$summary['rows'];
|
||||
|
||||
try {
|
||||
$settlementId = null;
|
||||
|
||||
DB::transaction(function () use ($batch, $row, &$settlementId) {
|
||||
$settlement = Settlement::withoutGlobalScopes()->withTrashed()->updateOrCreate(
|
||||
['store_id' => $batch->store_id, 'external_settlement_id' => $this->required($row, 'external_settlement_id')],
|
||||
[
|
||||
'company_id' => $batch->company_id,
|
||||
'period_start' => $this->date($row, 'period_start'),
|
||||
'period_end' => $this->date($row, 'period_end'),
|
||||
'settled_at' => $this->date($row, 'settled_at'),
|
||||
'currency_code' => strtoupper($this->value($row, 'currency_code') ?: 'CNY'),
|
||||
'gross_amount' => $this->money($row, 'gross_amount'),
|
||||
'fee_amount' => $this->money($row, 'fee_amount'),
|
||||
'refund_amount' => $this->money($row, 'refund_amount'),
|
||||
'net_amount' => $this->money($row, 'net_amount'),
|
||||
'status' => 'imported',
|
||||
'payload' => $row,
|
||||
'created_by' => $batch->created_by,
|
||||
'created_from' => 'ecommerce-finance:import',
|
||||
]
|
||||
);
|
||||
|
||||
if ($settlement->trashed()) {
|
||||
$settlement->restore();
|
||||
}
|
||||
|
||||
$item = SettlementLine::withoutGlobalScopes()->withTrashed()->updateOrCreate(
|
||||
['settlement_id' => $settlement->id, 'external_line_id' => $this->required($row, 'external_line_id')],
|
||||
[
|
||||
'company_id' => $batch->company_id,
|
||||
'line_type' => $this->required($row, 'line_type'),
|
||||
'external_order_id' => $this->value($row, 'external_order_id'),
|
||||
'amount' => $this->money($row, 'line_amount'),
|
||||
'occurred_at' => $this->date($row, 'occurred_at'),
|
||||
'payload' => $row,
|
||||
'created_by' => $batch->created_by,
|
||||
'created_from' => 'ecommerce-finance:import',
|
||||
]
|
||||
);
|
||||
|
||||
if ($item->trashed()) {
|
||||
$item->restore();
|
||||
}
|
||||
|
||||
$settlementId = $settlement->id;
|
||||
});
|
||||
|
||||
$summary['settlement_ids'][] = $settlementId;
|
||||
++$summary['success'];
|
||||
} catch (Throwable $exception) {
|
||||
$summary['errors'][] = ['line' => $line, 'message' => $exception->getMessage()];
|
||||
}
|
||||
}
|
||||
|
||||
$summary['settlement_ids'] = array_values(array_unique($summary['settlement_ids']));
|
||||
|
||||
return $summary;
|
||||
}
|
||||
|
||||
private function rows(ImportBatch $batch, array $requiredHeaders): iterable
|
||||
{
|
||||
$stream = Storage::disk('uploads')->readStream($batch->source_file);
|
||||
|
||||
if (! is_resource($stream)) {
|
||||
throw new RuntimeException('Unable to read the import source file.');
|
||||
}
|
||||
|
||||
try {
|
||||
$headers = fgetcsv($stream);
|
||||
|
||||
if ($headers === false) {
|
||||
throw new RuntimeException('The import source file is empty.');
|
||||
}
|
||||
|
||||
$headers = array_map(function ($header) {
|
||||
return trim(preg_replace('/^\xEF\xBB\xBF/', '', (string) $header));
|
||||
}, $headers);
|
||||
|
||||
$missingHeaders = array_diff($requiredHeaders, $headers);
|
||||
|
||||
if (! empty($missingHeaders)) {
|
||||
throw new RuntimeException('Missing CSV columns: ' . implode(', ', $missingHeaders));
|
||||
}
|
||||
|
||||
$line = 1;
|
||||
|
||||
while (($values = fgetcsv($stream)) !== false) {
|
||||
++$line;
|
||||
|
||||
if ($values === [null] || $values === []) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$values = array_pad($values, count($headers), null);
|
||||
yield $line => array_combine($headers, array_slice($values, 0, count($headers)));
|
||||
}
|
||||
} finally {
|
||||
fclose($stream);
|
||||
}
|
||||
}
|
||||
|
||||
private function required(array $row, string $field): string
|
||||
{
|
||||
$value = $this->value($row, $field);
|
||||
|
||||
if ($value === null || $value === '') {
|
||||
throw new RuntimeException("Missing required value: {$field}");
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
private function value(array $row, string $field): ?string
|
||||
{
|
||||
$value = $row[$field] ?? null;
|
||||
|
||||
return $value === null ? null : trim((string) $value);
|
||||
}
|
||||
|
||||
private function money(array $row, string $field): string
|
||||
{
|
||||
$value = $this->value($row, $field);
|
||||
|
||||
if ($value === null || $value === '') {
|
||||
return '0';
|
||||
}
|
||||
|
||||
if (! is_numeric($value)) {
|
||||
throw new RuntimeException("Invalid monetary value for {$field}");
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
private function number(array $row, string $field): string
|
||||
{
|
||||
$value = $this->required($row, $field);
|
||||
|
||||
if (! is_numeric($value)) {
|
||||
throw new RuntimeException("Invalid numeric value for {$field}");
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
private function date(array $row, string $field): ?string
|
||||
{
|
||||
$value = $this->value($row, $field);
|
||||
|
||||
if ($value === null || $value === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return Carbon::parse($value)->toDateTimeString();
|
||||
} catch (Throwable) {
|
||||
throw new RuntimeException("Invalid datetime value for {$field}");
|
||||
}
|
||||
}
|
||||
}
|
||||
26
modules/EcommerceFinance/Models/Channel.php
Normal file
26
modules/EcommerceFinance/Models/Channel.php
Normal file
@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\EcommerceFinance\Models;
|
||||
|
||||
use App\Abstracts\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class Channel extends Model
|
||||
{
|
||||
protected $table = 'ec_channels';
|
||||
|
||||
protected $fillable = [
|
||||
'company_id',
|
||||
'code',
|
||||
'name',
|
||||
'type',
|
||||
'enabled',
|
||||
'created_by',
|
||||
'created_from',
|
||||
];
|
||||
|
||||
public function stores(): HasMany
|
||||
{
|
||||
return $this->hasMany(Store::class);
|
||||
}
|
||||
}
|
||||
40
modules/EcommerceFinance/Models/ImportBatch.php
Normal file
40
modules/EcommerceFinance/Models/ImportBatch.php
Normal file
@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\EcommerceFinance\Models;
|
||||
|
||||
use App\Abstracts\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class ImportBatch extends Model
|
||||
{
|
||||
protected $table = 'ec_import_batches';
|
||||
|
||||
protected $fillable = [
|
||||
'company_id',
|
||||
'store_id',
|
||||
'source_type',
|
||||
'source_file',
|
||||
'checksum',
|
||||
'status',
|
||||
'row_count',
|
||||
'success_count',
|
||||
'error_count',
|
||||
'summary',
|
||||
'error_message',
|
||||
'started_at',
|
||||
'completed_at',
|
||||
'created_by',
|
||||
'created_from',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'summary' => 'array',
|
||||
'started_at' => 'datetime',
|
||||
'completed_at' => 'datetime',
|
||||
];
|
||||
|
||||
public function store(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Store::class);
|
||||
}
|
||||
}
|
||||
55
modules/EcommerceFinance/Models/Order.php
Normal file
55
modules/EcommerceFinance/Models/Order.php
Normal file
@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\EcommerceFinance\Models;
|
||||
|
||||
use App\Abstracts\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class Order extends Model
|
||||
{
|
||||
protected $table = 'ec_orders';
|
||||
|
||||
protected $fillable = [
|
||||
'company_id',
|
||||
'store_id',
|
||||
'external_order_id',
|
||||
'order_status',
|
||||
'ordered_at',
|
||||
'paid_at',
|
||||
'shipped_at',
|
||||
'completed_at',
|
||||
'currency_code',
|
||||
'gross_amount',
|
||||
'buyer_paid_amount',
|
||||
'platform_subsidy_amount',
|
||||
'merchant_subsidy_amount',
|
||||
'refunded_amount',
|
||||
'payload',
|
||||
'created_by',
|
||||
'created_from',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'ordered_at' => 'datetime',
|
||||
'paid_at' => 'datetime',
|
||||
'shipped_at' => 'datetime',
|
||||
'completed_at' => 'datetime',
|
||||
'payload' => 'array',
|
||||
];
|
||||
|
||||
public function store(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Store::class);
|
||||
}
|
||||
|
||||
public function items(): HasMany
|
||||
{
|
||||
return $this->hasMany(OrderItem::class);
|
||||
}
|
||||
|
||||
public function events(): HasMany
|
||||
{
|
||||
return $this->hasMany(OrderEvent::class);
|
||||
}
|
||||
}
|
||||
32
modules/EcommerceFinance/Models/OrderEvent.php
Normal file
32
modules/EcommerceFinance/Models/OrderEvent.php
Normal file
@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\EcommerceFinance\Models;
|
||||
|
||||
use App\Abstracts\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class OrderEvent extends Model
|
||||
{
|
||||
protected $table = 'ec_order_events';
|
||||
|
||||
protected $fillable = [
|
||||
'company_id',
|
||||
'order_id',
|
||||
'event_type',
|
||||
'external_event_id',
|
||||
'event_at',
|
||||
'payload',
|
||||
'created_by',
|
||||
'created_from',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'event_at' => 'datetime',
|
||||
'payload' => 'array',
|
||||
];
|
||||
|
||||
public function order(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Order::class);
|
||||
}
|
||||
}
|
||||
38
modules/EcommerceFinance/Models/OrderItem.php
Normal file
38
modules/EcommerceFinance/Models/OrderItem.php
Normal file
@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\EcommerceFinance\Models;
|
||||
|
||||
use App\Abstracts\Model;
|
||||
use App\Models\Common\Item;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class OrderItem extends Model
|
||||
{
|
||||
protected $table = 'ec_order_items';
|
||||
|
||||
protected $fillable = [
|
||||
'company_id',
|
||||
'order_id',
|
||||
'item_id',
|
||||
'external_line_id',
|
||||
'sku_code',
|
||||
'name',
|
||||
'quantity',
|
||||
'sale_amount',
|
||||
'discount_amount',
|
||||
'refund_amount',
|
||||
'cost_amount',
|
||||
'created_by',
|
||||
'created_from',
|
||||
];
|
||||
|
||||
public function order(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Order::class);
|
||||
}
|
||||
|
||||
public function item(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Item::class);
|
||||
}
|
||||
}
|
||||
38
modules/EcommerceFinance/Models/ReconciliationMatch.php
Normal file
38
modules/EcommerceFinance/Models/ReconciliationMatch.php
Normal file
@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\EcommerceFinance\Models;
|
||||
|
||||
use App\Abstracts\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class ReconciliationMatch extends Model
|
||||
{
|
||||
protected $table = 'ec_reconciliation_matches';
|
||||
|
||||
protected $fillable = [
|
||||
'company_id',
|
||||
'settlement_line_id',
|
||||
'order_id',
|
||||
'match_type',
|
||||
'matched_amount',
|
||||
'status',
|
||||
'rule_version',
|
||||
'confirmed_by',
|
||||
'confirmed_at',
|
||||
'note',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'confirmed_at' => 'datetime',
|
||||
];
|
||||
|
||||
public function settlementLine(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(SettlementLine::class);
|
||||
}
|
||||
|
||||
public function order(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Order::class);
|
||||
}
|
||||
}
|
||||
54
modules/EcommerceFinance/Models/Settlement.php
Normal file
54
modules/EcommerceFinance/Models/Settlement.php
Normal file
@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\EcommerceFinance\Models;
|
||||
|
||||
use App\Abstracts\Model;
|
||||
use App\Models\Banking\Transaction;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class Settlement extends Model
|
||||
{
|
||||
protected $table = 'ec_settlements';
|
||||
|
||||
protected $fillable = [
|
||||
'company_id',
|
||||
'store_id',
|
||||
'transaction_id',
|
||||
'external_settlement_id',
|
||||
'period_start',
|
||||
'period_end',
|
||||
'settled_at',
|
||||
'currency_code',
|
||||
'gross_amount',
|
||||
'fee_amount',
|
||||
'refund_amount',
|
||||
'net_amount',
|
||||
'status',
|
||||
'payload',
|
||||
'created_by',
|
||||
'created_from',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'period_start' => 'datetime',
|
||||
'period_end' => 'datetime',
|
||||
'settled_at' => 'datetime',
|
||||
'payload' => 'array',
|
||||
];
|
||||
|
||||
public function store(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Store::class);
|
||||
}
|
||||
|
||||
public function transaction(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Transaction::class);
|
||||
}
|
||||
|
||||
public function lines(): HasMany
|
||||
{
|
||||
return $this->hasMany(SettlementLine::class);
|
||||
}
|
||||
}
|
||||
40
modules/EcommerceFinance/Models/SettlementLine.php
Normal file
40
modules/EcommerceFinance/Models/SettlementLine.php
Normal file
@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\EcommerceFinance\Models;
|
||||
|
||||
use App\Abstracts\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class SettlementLine extends Model
|
||||
{
|
||||
protected $table = 'ec_settlement_lines';
|
||||
|
||||
protected $fillable = [
|
||||
'company_id',
|
||||
'settlement_id',
|
||||
'line_type',
|
||||
'external_line_id',
|
||||
'external_order_id',
|
||||
'amount',
|
||||
'occurred_at',
|
||||
'payload',
|
||||
'created_by',
|
||||
'created_from',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'occurred_at' => 'datetime',
|
||||
'payload' => 'array',
|
||||
];
|
||||
|
||||
public function settlement(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Settlement::class);
|
||||
}
|
||||
|
||||
public function matches(): HasMany
|
||||
{
|
||||
return $this->hasMany(ReconciliationMatch::class);
|
||||
}
|
||||
}
|
||||
33
modules/EcommerceFinance/Models/SkuMapping.php
Normal file
33
modules/EcommerceFinance/Models/SkuMapping.php
Normal file
@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\EcommerceFinance\Models;
|
||||
|
||||
use App\Abstracts\Model;
|
||||
use App\Models\Common\Item;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class SkuMapping extends Model
|
||||
{
|
||||
protected $table = 'ec_sku_mappings';
|
||||
|
||||
protected $fillable = [
|
||||
'company_id',
|
||||
'store_id',
|
||||
'item_id',
|
||||
'external_sku_id',
|
||||
'external_sku_code',
|
||||
'external_sku_name',
|
||||
'created_by',
|
||||
'created_from',
|
||||
];
|
||||
|
||||
public function store(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Store::class);
|
||||
}
|
||||
|
||||
public function item(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Item::class);
|
||||
}
|
||||
}
|
||||
51
modules/EcommerceFinance/Models/Store.php
Normal file
51
modules/EcommerceFinance/Models/Store.php
Normal file
@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\EcommerceFinance\Models;
|
||||
|
||||
use App\Abstracts\Model;
|
||||
use App\Models\Banking\Account;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class Store extends Model
|
||||
{
|
||||
protected $table = 'ec_stores';
|
||||
|
||||
protected $fillable = [
|
||||
'company_id',
|
||||
'channel_id',
|
||||
'name',
|
||||
'external_id',
|
||||
'settlement_account_id',
|
||||
'timezone',
|
||||
'currency_code',
|
||||
'enabled',
|
||||
'created_by',
|
||||
'created_from',
|
||||
];
|
||||
|
||||
public function channel(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Channel::class);
|
||||
}
|
||||
|
||||
public function settlementAccount(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Account::class, 'settlement_account_id');
|
||||
}
|
||||
|
||||
public function importBatches(): HasMany
|
||||
{
|
||||
return $this->hasMany(ImportBatch::class);
|
||||
}
|
||||
|
||||
public function orders(): HasMany
|
||||
{
|
||||
return $this->hasMany(Order::class);
|
||||
}
|
||||
|
||||
public function settlements(): HasMany
|
||||
{
|
||||
return $this->hasMany(Settlement::class);
|
||||
}
|
||||
}
|
||||
18
modules/EcommerceFinance/Providers/Event.php
Normal file
18
modules/EcommerceFinance/Providers/Event.php
Normal file
@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\EcommerceFinance\Providers;
|
||||
|
||||
use Illuminate\Foundation\Support\Providers\EventServiceProvider as Provider;
|
||||
|
||||
class Event extends Provider
|
||||
{
|
||||
public function shouldDiscoverEvents(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function discoverEventsWithin(): array
|
||||
{
|
||||
return [__DIR__ . '/../Listeners'];
|
||||
}
|
||||
}
|
||||
25
modules/EcommerceFinance/Providers/Main.php
Normal file
25
modules/EcommerceFinance/Providers/Main.php
Normal file
@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\EcommerceFinance\Providers;
|
||||
|
||||
use Illuminate\Support\ServiceProvider as Provider;
|
||||
|
||||
class Main extends Provider
|
||||
{
|
||||
public function boot(): void
|
||||
{
|
||||
$this->loadTranslationsFrom(__DIR__ . '/../Resources/lang', 'ecommerce-finance');
|
||||
$this->loadMigrationsFrom(__DIR__ . '/../Database/Migrations');
|
||||
}
|
||||
|
||||
public function register(): void
|
||||
{
|
||||
if (app()->routesAreCached()) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (['admin.php', 'api.php'] as $route) {
|
||||
$this->loadRoutesFrom(__DIR__ . '/../Routes/' . $route);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,6 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'name' => 'E-commerce Finance',
|
||||
'description' => 'Reconcile marketplace settlements and calculate e-commerce profitability.',
|
||||
];
|
||||
@ -0,0 +1,6 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'name' => '电商财务',
|
||||
'description' => '管理平台结算、差异对账与电商利润核算。',
|
||||
];
|
||||
11
modules/EcommerceFinance/Resources/views/index.blade.php
Normal file
11
modules/EcommerceFinance/Resources/views/index.blade.php
Normal file
@ -0,0 +1,11 @@
|
||||
<x-layouts.admin>
|
||||
<x-slot name="title">{{ trans('ecommerce-finance::general.name') }}</x-slot>
|
||||
|
||||
<x-slot name="content">
|
||||
<x-empty-page
|
||||
page="module"
|
||||
title="{{ trans('ecommerce-finance::general.name') }}"
|
||||
description="{{ trans('ecommerce-finance::general.description') }}"
|
||||
/>
|
||||
</x-slot>
|
||||
</x-layouts.admin>
|
||||
7
modules/EcommerceFinance/Routes/admin.php
Normal file
7
modules/EcommerceFinance/Routes/admin.php
Normal file
@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::admin('ecommerce-finance', function () {
|
||||
Route::view('/', 'ecommerce-finance::index')->name('index');
|
||||
});
|
||||
11
modules/EcommerceFinance/Routes/api.php
Normal file
11
modules/EcommerceFinance/Routes/api.php
Normal file
@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::api('ecommerce-finance', function () {
|
||||
Route::post('import-files', 'ImportFiles@store')->name('import-files.store');
|
||||
Route::apiResource('channels', 'Channels')->except(['destroy']);
|
||||
Route::apiResource('stores', 'Stores')->except(['destroy']);
|
||||
Route::apiResource('import-batches', 'ImportBatches')->only(['index', 'show', 'store']);
|
||||
Route::get('reconciliation-exceptions', 'ReconciliationExceptions@index')->name('reconciliation-exceptions.index');
|
||||
});
|
||||
140
modules/EcommerceFinance/Tests/Feature/ChannelsTest.php
Normal file
140
modules/EcommerceFinance/Tests/Feature/ChannelsTest.php
Normal file
@ -0,0 +1,140 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\EcommerceFinance\Tests\Feature;
|
||||
|
||||
use Modules\EcommerceFinance\Models\Channel;
|
||||
use Modules\EcommerceFinance\Models\Store;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Tests\Feature\FeatureTestCase;
|
||||
|
||||
class ChannelsTest extends FeatureTestCase
|
||||
{
|
||||
public function test_it_uploads_a_private_csv_file_for_import(): void
|
||||
{
|
||||
$this->loginAs();
|
||||
Storage::fake('uploads');
|
||||
|
||||
$file = UploadedFile::fake()->createWithContent('orders.csv', implode("\n", [
|
||||
'external_order_id,external_line_id,order_status,item_name,quantity',
|
||||
'order-001,line-001,completed,Wireless Mouse,1',
|
||||
]));
|
||||
|
||||
$response = $this->withHeaders($this->apiHeaders())
|
||||
->post(route('api.ecommerce-finance.import-files.store'), ['source_file' => $file])
|
||||
->assertCreated()
|
||||
->assertJsonStructure(['source_file', 'checksum']);
|
||||
|
||||
Storage::disk('uploads')->assertExists($response->json('source_file'));
|
||||
$this->assertSame(hash_file('sha256', $file->getRealPath()), $response->json('checksum'));
|
||||
}
|
||||
|
||||
public function test_it_creates_a_channel_for_the_current_company(): void
|
||||
{
|
||||
$request = [
|
||||
'code' => 'taobao',
|
||||
'name' => 'Taobao',
|
||||
'type' => 'marketplace',
|
||||
'enabled' => true,
|
||||
];
|
||||
|
||||
$this->loginAs();
|
||||
|
||||
$this->withHeaders($this->apiHeaders())
|
||||
->postJson(route('api.ecommerce-finance.channels.store'), $request)
|
||||
->assertCreated()
|
||||
->assertJsonPath('company_id', company_id())
|
||||
->assertJsonPath('code', 'taobao');
|
||||
|
||||
$this->assertDatabaseHas('ec_channels', [
|
||||
'company_id' => company_id(),
|
||||
'code' => 'taobao',
|
||||
'name' => 'Taobao',
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_it_rejects_a_duplicate_import_batch(): void
|
||||
{
|
||||
$this->loginAs();
|
||||
Storage::fake('uploads');
|
||||
|
||||
$channel = Channel::create([
|
||||
'company_id' => company_id(),
|
||||
'code' => 'jd',
|
||||
'name' => 'JD',
|
||||
'type' => 'marketplace',
|
||||
]);
|
||||
|
||||
$store = Store::create([
|
||||
'company_id' => company_id(),
|
||||
'channel_id' => $channel->id,
|
||||
'name' => 'JD Flagship Store',
|
||||
'external_id' => 'jd-flagship',
|
||||
]);
|
||||
|
||||
$sourceFile = 'ecommerce-finance/imports/jd-orders-20260718.csv';
|
||||
|
||||
Storage::disk('uploads')->put($sourceFile, implode("\n", [
|
||||
'external_order_id,external_line_id,order_status,item_name,quantity,sale_amount,gross_amount,buyer_paid_amount,currency_code,completed_at',
|
||||
'order-001,line-001,completed,Wireless Mouse,2,99.80,99.80,99.80,CNY,2026-07-18 10:00:00',
|
||||
]));
|
||||
|
||||
$request = [
|
||||
'store_id' => $store->id,
|
||||
'source_type' => 'orders',
|
||||
'source_file' => $sourceFile,
|
||||
'checksum' => str_repeat('a', 64),
|
||||
];
|
||||
|
||||
$this->withHeaders($this->apiHeaders())
|
||||
->postJson(route('api.ecommerce-finance.import-batches.store'), $request)
|
||||
->assertCreated()
|
||||
->assertJsonPath('status', 'completed');
|
||||
|
||||
$this->assertDatabaseHas('ec_orders', [
|
||||
'company_id' => company_id(),
|
||||
'store_id' => $store->id,
|
||||
'external_order_id' => 'order-001',
|
||||
]);
|
||||
$this->assertDatabaseHas('ec_order_items', [
|
||||
'company_id' => company_id(),
|
||||
'external_line_id' => 'line-001',
|
||||
'name' => 'Wireless Mouse',
|
||||
]);
|
||||
|
||||
$settlementFile = 'ecommerce-finance/imports/jd-settlement-20260718.csv';
|
||||
Storage::disk('uploads')->put($settlementFile, implode("\n", [
|
||||
'external_settlement_id,external_line_id,line_type,external_order_id,line_amount,currency_code,settled_at',
|
||||
'settlement-001,settlement-line-001,order_payment,order-001,99.80,CNY,2026-07-19 10:00:00',
|
||||
]));
|
||||
|
||||
$this->withHeaders($this->apiHeaders())
|
||||
->postJson(route('api.ecommerce-finance.import-batches.store'), [
|
||||
'store_id' => $store->id,
|
||||
'source_type' => 'settlements',
|
||||
'source_file' => $settlementFile,
|
||||
'checksum' => str_repeat('b', 64),
|
||||
])
|
||||
->assertCreated()
|
||||
->assertJsonPath('status', 'completed');
|
||||
|
||||
$this->assertDatabaseHas('ec_reconciliation_matches', [
|
||||
'company_id' => company_id(),
|
||||
'status' => 'auto_matched',
|
||||
'matched_amount' => '99.8',
|
||||
]);
|
||||
|
||||
$this->withExceptionHandling();
|
||||
|
||||
$this->withHeaders($this->apiHeaders())
|
||||
->postJson(route('api.ecommerce-finance.import-batches.store'), $request)
|
||||
->assertBadRequest();
|
||||
}
|
||||
|
||||
private function apiHeaders(): array
|
||||
{
|
||||
return [
|
||||
'Authorization' => 'Basic ' . base64_encode('test@company.com:123456'),
|
||||
];
|
||||
}
|
||||
}
|
||||
18
modules/EcommerceFinance/module.json
Normal file
18
modules/EcommerceFinance/module.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"alias": "ecommerce-finance",
|
||||
"icon": "shopping-cart",
|
||||
"version": "0.1.0",
|
||||
"active": 1,
|
||||
"providers": [
|
||||
"Modules\\EcommerceFinance\\Providers\\Event",
|
||||
"Modules\\EcommerceFinance\\Providers\\Main"
|
||||
],
|
||||
"aliases": {},
|
||||
"files": [],
|
||||
"requires": [],
|
||||
"reports": [],
|
||||
"widgets": [],
|
||||
"settings": [],
|
||||
"extra-modules": {},
|
||||
"routes": []
|
||||
}
|
||||
50
modules/OfflinePayments/.github/workflows/tests.yml
vendored
Normal file
50
modules/OfflinePayments/.github/workflows/tests.yml
vendored
Normal file
@ -0,0 +1,50 @@
|
||||
name: Tests
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 0 * * 0'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
|
||||
name: PHP ${{ matrix.php }}
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
php: ['7.3', '7.4']
|
||||
|
||||
steps:
|
||||
- name: Checkout Akaunting
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
repository: akaunting/akaunting
|
||||
|
||||
- name: Checkout Offline Payments module
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
path: modules/OfflinePayments
|
||||
|
||||
- name: Cache Composer
|
||||
uses: actions/cache@v1
|
||||
with:
|
||||
path: ~/.composer/cache/files
|
||||
key: php-${{ matrix.php }}-composer-${{ hashFiles('composer.json') }}
|
||||
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: ${{ matrix.php }}
|
||||
extensions: bcmath, ctype, dom, fileinfo, intl, gd, json, mbstring, pdo, pdo_sqlite, openssl, sqlite, xml, zip
|
||||
coverage: none
|
||||
|
||||
- name: Copy .env
|
||||
run: cp .env.testing .env
|
||||
|
||||
- name: Install Composer
|
||||
run: cd modules/OfflinePayments ; composer test ; cd ../.. ; composer test
|
||||
|
||||
- name: Execute tests
|
||||
run: php artisan test --parallel
|
||||
37
modules/OfflinePayments/.github/workflows/translations.yml
vendored
Normal file
37
modules/OfflinePayments/.github/workflows/translations.yml
vendored
Normal file
@ -0,0 +1,37 @@
|
||||
name: Translations
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 0 * * 0'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
sync:
|
||||
name: Sync
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Sync with Crowdin
|
||||
uses: crowdin/github-action@master
|
||||
with:
|
||||
upload_sources: true
|
||||
upload_translations: true
|
||||
download_translations: true
|
||||
skip_untranslated_files: true
|
||||
|
||||
localization_branch_name: 'translations'
|
||||
commit_message: 'new crowdin translations'
|
||||
pull_request_title: 'New Crowdin translations'
|
||||
pull_request_body: 'https://crowdin.com/project/akaunting-apps'
|
||||
pull_request_labels: 'Translation'
|
||||
|
||||
config: 'crowdin.yml'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_APPS_ID }}
|
||||
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
|
||||
7
modules/OfflinePayments/.gitignore
vendored
Normal file
7
modules/OfflinePayments/.gitignore
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
/.idea
|
||||
/.history
|
||||
/.vscode
|
||||
/.vagrant
|
||||
/node_modules
|
||||
/storage/*.key
|
||||
/vendor
|
||||
89
modules/OfflinePayments/Http/Controllers/Payment.php
Normal file
89
modules/OfflinePayments/Http/Controllers/Payment.php
Normal file
@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\OfflinePayments\Http\Controllers;
|
||||
|
||||
use App\Abstracts\Http\PaymentController;
|
||||
use \App\Events\Document\PaymentReceived;
|
||||
use App\Http\Requests\Portal\InvoicePayment as PaymentRequest;
|
||||
use App\Models\Document\Document;
|
||||
use App\Utilities\Modules;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class Payment extends PaymentController
|
||||
{
|
||||
public $alias = 'offline-payments';
|
||||
|
||||
public $type = 'redirect';
|
||||
|
||||
public function show(Document $invoice, PaymentRequest $request, $cards = [])
|
||||
{
|
||||
$setting = [];
|
||||
|
||||
$payment_methods = json_decode(setting('offline-payments.methods'), true);
|
||||
|
||||
foreach ($payment_methods as $payment_method) {
|
||||
if ($payment_method['code'] == $request['payment_method']) {
|
||||
$setting = $payment_method;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($setting)) {
|
||||
Modules::clearPaymentMethodsCache();
|
||||
|
||||
return response()->json([
|
||||
'code' => '',
|
||||
'name' => 'N/A',
|
||||
'description' => '',
|
||||
'redirect' => redirect()->back()->getTargetUrl(),
|
||||
'html' => '',
|
||||
]);
|
||||
}
|
||||
|
||||
$confirm_url = $this->getConfirmUrl($invoice);
|
||||
|
||||
$html = view('offline-payments::show', compact('setting', 'invoice', 'confirm_url'))->render();
|
||||
|
||||
return response()->json([
|
||||
'code' => $setting['code'],
|
||||
'name' => $setting['name'],
|
||||
'description' => $setting['description'],
|
||||
'redirect' => false,
|
||||
'html' => $html,
|
||||
]);
|
||||
}
|
||||
|
||||
public function confirm(Document $invoice, Request $request)
|
||||
{
|
||||
try {
|
||||
event(new PaymentReceived($invoice, $request));
|
||||
|
||||
$message = trans('messages.success.added', ['type' => trans_choice('general.payments', 1)]);
|
||||
|
||||
flash($message)->success();
|
||||
|
||||
$response = [
|
||||
'success' => true,
|
||||
'error' => false,
|
||||
'message' => $message,
|
||||
'data' => false,
|
||||
'redirect' => $this->getFinishUrl($invoice),
|
||||
];
|
||||
} catch(\Exception $e) {
|
||||
$message = $e->getMessage();
|
||||
|
||||
flash($message)->error()->important();
|
||||
|
||||
$response = [
|
||||
'success' => false,
|
||||
'error' => true,
|
||||
'message' => $message,
|
||||
'data' => false,
|
||||
'redirect' => $this->getInvoiceUrl($invoice),
|
||||
];
|
||||
}
|
||||
|
||||
return response()->json($response);
|
||||
}
|
||||
}
|
||||
120
modules/OfflinePayments/Http/Controllers/Settings.php
Normal file
120
modules/OfflinePayments/Http/Controllers/Settings.php
Normal file
@ -0,0 +1,120 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\OfflinePayments\Http\Controllers;
|
||||
|
||||
use App\Abstracts\Http\Controller;
|
||||
use Illuminate\Http\Response;
|
||||
use Modules\OfflinePayments\Http\Requests\Setting as Request;
|
||||
use Modules\OfflinePayments\Http\Requests\SettingGet as GRequest;
|
||||
use Modules\OfflinePayments\Http\Requests\SettingDelete as DRequest;
|
||||
use Modules\OfflinePayments\Jobs\CreatePaymentMethod;
|
||||
use Modules\OfflinePayments\Jobs\DeletePaymentMethod;
|
||||
use Modules\OfflinePayments\Jobs\UpdatePaymentMethod;
|
||||
|
||||
class Settings extends Controller
|
||||
{
|
||||
/**
|
||||
* Show the form for editing the specified resource.
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function edit()
|
||||
{
|
||||
$methods = json_decode(setting('offline-payments.methods'));
|
||||
|
||||
return view('offline-payments::edit', compact('methods'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the specified resource in storage.
|
||||
*
|
||||
* @param Request $request
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function update(Request $request)
|
||||
{
|
||||
$code_exists = false;
|
||||
$methods = json_decode(setting('offline-payments.methods'), true);
|
||||
|
||||
if (in_array($request->get('update_code'), array_column($methods, 'code'))) {
|
||||
$code_exists = true;
|
||||
}
|
||||
|
||||
if (!empty($request->get('update_code')) && $code_exists == true) {
|
||||
$payment_method = $this->dispatch(new UpdatePaymentMethod($request));
|
||||
|
||||
$message = trans('messages.success.updated', ['type' => $payment_method['name']]);
|
||||
} else {
|
||||
$payment_method = $this->dispatch(new CreatePaymentMethod($request));
|
||||
|
||||
$message = trans('messages.success.added', ['type' => $payment_method['name']]);
|
||||
}
|
||||
|
||||
flash($message)->success();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'error' => false,
|
||||
'message' => $message,
|
||||
'redirect' => route('offline-payments.settings.edit'),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the specified resource from storage.
|
||||
*
|
||||
* @param GRequest $request
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function get(GRequest $request)
|
||||
{
|
||||
$data = [];
|
||||
|
||||
$code = $request->get('code');
|
||||
|
||||
$methods = json_decode(setting('offline-payments.methods'), true);
|
||||
|
||||
foreach ($methods as $key => $method) {
|
||||
if ($method['code'] != $code) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$method['title'] = trans('offline-payments::offline-payments.edit', ['method' => $method['name']]);
|
||||
$method['update_code'] = $code;
|
||||
|
||||
$code = explode('.', $method['code']);
|
||||
|
||||
$method['code'] = $code[1];
|
||||
|
||||
$data = $method;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'errors' => false,
|
||||
'success' => true,
|
||||
'data' => $data,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the specified resource from storage.
|
||||
*
|
||||
* @param DRequest $request
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function destroy(DRequest $request)
|
||||
{
|
||||
$response = $this->ajaxDispatch(new DeletePaymentMethod($request));
|
||||
|
||||
if ($response['success']) {
|
||||
$response['message'] = trans('messages.success.deleted', ['type' => $response['data']['name']]);
|
||||
}
|
||||
|
||||
return response()->json($response);
|
||||
}
|
||||
}
|
||||
30
modules/OfflinePayments/Http/Requests/Setting.php
Normal file
30
modules/OfflinePayments/Http/Requests/Setting.php
Normal file
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\OfflinePayments\Http\Requests;
|
||||
|
||||
use App\Abstracts\Http\FormRequest as Request;
|
||||
|
||||
class Setting extends Request
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function authorize()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function rules()
|
||||
{
|
||||
return [
|
||||
'name' => 'required|string',
|
||||
];
|
||||
}
|
||||
}
|
||||
30
modules/OfflinePayments/Http/Requests/SettingDelete.php
Normal file
30
modules/OfflinePayments/Http/Requests/SettingDelete.php
Normal file
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\OfflinePayments\Http\Requests;
|
||||
|
||||
use App\Abstracts\Http\FormRequest as Request;
|
||||
|
||||
class SettingDelete extends Request
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function authorize()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function rules()
|
||||
{
|
||||
return [
|
||||
'code' => 'required|string',
|
||||
];
|
||||
}
|
||||
}
|
||||
30
modules/OfflinePayments/Http/Requests/SettingGet.php
Normal file
30
modules/OfflinePayments/Http/Requests/SettingGet.php
Normal file
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\OfflinePayments\Http\Requests;
|
||||
|
||||
use App\Abstracts\Http\FormRequest as Request;
|
||||
|
||||
class SettingGet extends Request
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function authorize()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function rules()
|
||||
{
|
||||
return [
|
||||
'code' => 'required|string',
|
||||
];
|
||||
}
|
||||
}
|
||||
30
modules/OfflinePayments/Http/Requests/Show.php
Normal file
30
modules/OfflinePayments/Http/Requests/Show.php
Normal file
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\OfflinePayments\Http\Requests;
|
||||
|
||||
use App\Abstracts\Http\FormRequest as Request;
|
||||
|
||||
class Show extends Request
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function authorize()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function rules()
|
||||
{
|
||||
return [
|
||||
'payment_method' => 'required|string',
|
||||
];
|
||||
}
|
||||
}
|
||||
58
modules/OfflinePayments/Jobs/CreatePaymentMethod.php
Normal file
58
modules/OfflinePayments/Jobs/CreatePaymentMethod.php
Normal file
@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\OfflinePayments\Jobs;
|
||||
|
||||
use App\Abstracts\Job;
|
||||
use App\Utilities\Modules;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class CreatePaymentMethod extends Job
|
||||
{
|
||||
protected $request;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @param $request
|
||||
*/
|
||||
public function __construct($request)
|
||||
{
|
||||
$this->request = $this->getRequestInstance($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$methods = json_decode(setting('offline-payments.methods'), true);
|
||||
|
||||
$code = 'offline-payments.' . Str::slug($this->request->get('name'), '_') . '.' . (count($methods) + 1);
|
||||
|
||||
foreach ($methods as $method) {
|
||||
if (Str::contains($method['code'], 'offline-payments.' . Str::slug($this->request->get('name'), '_') . '.')) {
|
||||
return $method;
|
||||
}
|
||||
}
|
||||
|
||||
$payment_method = [
|
||||
'code' => $code,
|
||||
'name' => $this->request->get('name'),
|
||||
'customer' => $this->request->get('customer'),
|
||||
'order' => $this->request->get('order'),
|
||||
'description' => $this->request->get('description'),
|
||||
];
|
||||
|
||||
$methods[] = $payment_method;
|
||||
|
||||
setting()->set('offline-payments.methods', json_encode($methods));
|
||||
|
||||
setting()->save();
|
||||
|
||||
Modules::clearPaymentMethodsCache();
|
||||
|
||||
return $payment_method;
|
||||
}
|
||||
}
|
||||
85
modules/OfflinePayments/Jobs/DeletePaymentMethod.php
Normal file
85
modules/OfflinePayments/Jobs/DeletePaymentMethod.php
Normal file
@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\OfflinePayments\Jobs;
|
||||
|
||||
use App\Abstracts\Job;
|
||||
use App\Utilities\Modules;
|
||||
use App\Models\Banking\Transaction;
|
||||
use App\Models\Banking\Transfer;
|
||||
|
||||
class DeletePaymentMethod extends Job
|
||||
{
|
||||
protected $request;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @param $request
|
||||
*/
|
||||
public function __construct($request)
|
||||
{
|
||||
$this->request = $this->getRequestInstance($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$this->authorize();
|
||||
|
||||
$methods = json_decode(setting('offline-payments.methods'), true);
|
||||
|
||||
$payment_method = [];
|
||||
|
||||
$code = $this->request->get('code');
|
||||
|
||||
foreach ($methods as $key => $method) {
|
||||
if ($method['code'] != $code) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$payment_method = $methods[$key];
|
||||
|
||||
unset($methods[$key]);
|
||||
}
|
||||
|
||||
setting()->set('offline-payments.methods', json_encode($methods));
|
||||
|
||||
setting()->save();
|
||||
|
||||
Modules::clearPaymentMethodsCache();
|
||||
|
||||
return $payment_method;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if this action is applicable.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function authorize()
|
||||
{
|
||||
if ($relationships = $this->getRelationships()) {
|
||||
$methods = json_decode(setting('offline-payments.methods'), true);
|
||||
$method = $methods[array_search($this->request->get('code'), array_column($methods, 'code'))];
|
||||
|
||||
$message = trans('messages.warning.deleted', ['name' => $method['name'], 'text' => implode(', ', $relationships)]);
|
||||
|
||||
throw new \Exception($message);
|
||||
}
|
||||
}
|
||||
|
||||
public function getRelationships()
|
||||
{
|
||||
$counter = [];
|
||||
|
||||
if ($transaction_count = Transaction::where('payment_method', $this->request->get('code'))->count()) {
|
||||
$counter[] = $transaction_count . ' ' . strtolower(trans_choice('general.transactions', ($transaction_count > 1) ? 2 : 1));
|
||||
}
|
||||
|
||||
return $counter;
|
||||
}
|
||||
}
|
||||
59
modules/OfflinePayments/Jobs/UpdatePaymentMethod.php
Normal file
59
modules/OfflinePayments/Jobs/UpdatePaymentMethod.php
Normal file
@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\OfflinePayments\Jobs;
|
||||
|
||||
use App\Abstracts\Job;
|
||||
use App\Utilities\Modules;
|
||||
|
||||
class UpdatePaymentMethod extends Job
|
||||
{
|
||||
protected $request;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @param $request
|
||||
*/
|
||||
public function __construct($request)
|
||||
{
|
||||
$this->request = $this->getRequestInstance($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$methods = json_decode(setting('offline-payments.methods'), true);
|
||||
|
||||
$payment_method = [];
|
||||
|
||||
$code = $this->request->get('update_code');
|
||||
|
||||
foreach ($methods as $key => $method) {
|
||||
if ($method['code'] != $code) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$payment_method = [
|
||||
'code' => $code,
|
||||
'name' => $this->request->get('name'),
|
||||
'customer' => $this->request->get('customer'),
|
||||
'order' => $this->request->get('order'),
|
||||
'description' => $this->request->get('description'),
|
||||
];
|
||||
|
||||
$methods[$key] = $payment_method;
|
||||
}
|
||||
|
||||
setting()->set('offline-payments.methods', json_encode($methods));
|
||||
|
||||
setting()->save();
|
||||
|
||||
Modules::clearPaymentMethodsCache();
|
||||
|
||||
return $payment_method;
|
||||
}
|
||||
}
|
||||
19
modules/OfflinePayments/Listeners/AddLandingPage.php
Normal file
19
modules/OfflinePayments/Listeners/AddLandingPage.php
Normal file
@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\OfflinePayments\Listeners;
|
||||
|
||||
use App\Events\Auth\LandingPageShowing as Event;
|
||||
|
||||
class AddLandingPage
|
||||
{
|
||||
/**
|
||||
* Handle the event.
|
||||
*
|
||||
* @param Event $event
|
||||
* @return void
|
||||
*/
|
||||
public function handle(Event $event)
|
||||
{
|
||||
$event->user->landing_pages['offline-payments.settings.edit'] = trans('offline-payments::general.name');
|
||||
}
|
||||
}
|
||||
36
modules/OfflinePayments/Listeners/FinishInstallation.php
Normal file
36
modules/OfflinePayments/Listeners/FinishInstallation.php
Normal file
@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\OfflinePayments\Listeners;
|
||||
|
||||
use App\Events\Module\Installed as Event;
|
||||
use App\Traits\Permissions;
|
||||
|
||||
class FinishInstallation
|
||||
{
|
||||
use Permissions;
|
||||
|
||||
public $alias = 'offline-payments';
|
||||
|
||||
/**
|
||||
* Handle the event.
|
||||
*
|
||||
* @param Event $event
|
||||
* @return void
|
||||
*/
|
||||
public function handle(Event $event)
|
||||
{
|
||||
if ($event->alias != $this->alias) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->updatePermissions();
|
||||
}
|
||||
|
||||
protected function updatePermissions()
|
||||
{
|
||||
// c=create, r=read, u=update, d=delete
|
||||
$this->attachPermissionsToAdminRoles([
|
||||
$this->alias . '-settings' => 'r,u,d',
|
||||
]);
|
||||
}
|
||||
}
|
||||
27
modules/OfflinePayments/Listeners/ShowAsPaymentMethod.php
Normal file
27
modules/OfflinePayments/Listeners/ShowAsPaymentMethod.php
Normal file
@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\OfflinePayments\Listeners;
|
||||
|
||||
use App\Events\Module\PaymentMethodShowing as Event;
|
||||
|
||||
class ShowAsPaymentMethod
|
||||
{
|
||||
/**
|
||||
* Handle the event.
|
||||
*
|
||||
* @param Event $event
|
||||
* @return void
|
||||
*/
|
||||
public function handle(Event $event)
|
||||
{
|
||||
$methods = json_decode(setting('offline-payments.methods'), true);
|
||||
|
||||
if (empty($methods)) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($methods as $method) {
|
||||
$event->modules->payment_methods[] = $method;
|
||||
}
|
||||
}
|
||||
}
|
||||
31
modules/OfflinePayments/Listeners/ShowInSettingsMenu.php
Normal file
31
modules/OfflinePayments/Listeners/ShowInSettingsMenu.php
Normal file
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\OfflinePayments\Listeners;
|
||||
|
||||
use App\Events\Menu\SettingsCreated as Event;
|
||||
use App\Traits\Modules;
|
||||
use App\Traits\Permissions;
|
||||
|
||||
class ShowInSettingsMenu
|
||||
{
|
||||
use Modules, Permissions;
|
||||
|
||||
/**
|
||||
* Handle the event.
|
||||
*
|
||||
* @param Event $event
|
||||
* @return void
|
||||
*/
|
||||
public function handle(Event $event)
|
||||
{
|
||||
if (!$this->moduleIsEnabled('offline-payments')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$title = trans('offline-payments::general.name');
|
||||
|
||||
if ($this->canAccessMenuItem($title, 'read-offline-payments-settings')) {
|
||||
$event->menu->route('offline-payments.settings.edit', $title, [], 100, ['icon' => 'credit_card', 'search_keywords' => trans('offline-payments::general.description')]);
|
||||
}
|
||||
}
|
||||
}
|
||||
34
modules/OfflinePayments/Listeners/Update/Version300.php
Normal file
34
modules/OfflinePayments/Listeners/Update/Version300.php
Normal file
@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\OfflinePayments\Listeners\Update;
|
||||
|
||||
use App\Abstracts\Listeners\Update as Listener;
|
||||
use App\Events\Install\UpdateFinished;
|
||||
use Illuminate\Support\Facades\File;
|
||||
|
||||
class Version300 extends Listener
|
||||
{
|
||||
const ALIAS = 'offline-payments';
|
||||
|
||||
const VERSION = '3.0.0';
|
||||
|
||||
/**
|
||||
* Handle the event.
|
||||
*
|
||||
* @param $event
|
||||
* @return void
|
||||
*/
|
||||
public function handle(UpdateFinished $event)
|
||||
{
|
||||
if ($this->skipThisUpdate($event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->deleteOldFiles();
|
||||
}
|
||||
|
||||
protected function deleteOldFiles()
|
||||
{
|
||||
File::delete(base_path('modules/OfflinePayments/Listeners/ShowInSettingsPage.php'));
|
||||
}
|
||||
}
|
||||
30
modules/OfflinePayments/Providers/Event.php
Normal file
30
modules/OfflinePayments/Providers/Event.php
Normal file
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\OfflinePayments\Providers;
|
||||
|
||||
use Illuminate\Foundation\Support\Providers\EventServiceProvider as Provider;
|
||||
|
||||
class Event extends Provider
|
||||
{
|
||||
/**
|
||||
* Determine if events and listeners should be automatically discovered.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function shouldDiscoverEvents()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the listener directories that should be used to discover events.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function discoverEventsWithin()
|
||||
{
|
||||
return [
|
||||
__DIR__ . '/../Listeners',
|
||||
];
|
||||
}
|
||||
}
|
||||
81
modules/OfflinePayments/Providers/Main.php
Normal file
81
modules/OfflinePayments/Providers/Main.php
Normal file
@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\OfflinePayments\Providers;
|
||||
|
||||
use Illuminate\Support\ServiceProvider as Provider;
|
||||
|
||||
class Main extends Provider
|
||||
{
|
||||
/**
|
||||
* Boot the application events.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function boot()
|
||||
{
|
||||
$this->loadTranslations();
|
||||
$this->loadViews();
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the service provider.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register()
|
||||
{
|
||||
$this->loadRoutes();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load views.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function loadViews()
|
||||
{
|
||||
$this->loadViewsFrom(__DIR__ . '/../Resources/views', 'offline-payments');
|
||||
}
|
||||
|
||||
/**
|
||||
* Load translations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function loadTranslations()
|
||||
{
|
||||
$this->loadTranslationsFrom(__DIR__ . '/../Resources/lang', 'offline-payments');
|
||||
}
|
||||
|
||||
/**
|
||||
* Load routes.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function loadRoutes()
|
||||
{
|
||||
if (app()->routesAreCached()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$routes = [
|
||||
'admin.php',
|
||||
'portal.php',
|
||||
'signed.php',
|
||||
];
|
||||
|
||||
foreach ($routes as $route) {
|
||||
$this->loadRoutesFrom(__DIR__ . '/../Routes/' . $route);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the services provided by the provider.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function provides()
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
11
modules/OfflinePayments/README.md
Normal file
11
modules/OfflinePayments/README.md
Normal file
@ -0,0 +1,11 @@
|
||||
# Offline Payments app for Akaunting
|
||||
|
||||
[](https://github.com/akaunting/module-offline-payments/actions)
|
||||
|
||||
## Tests
|
||||
|
||||
The workflow runs both [Akaunting](https://github.com/akaunting/akaunting/tree/master/tests) and module test suites. They're configured to run once per week and triggered manually. Therefore, **before** publishing a new release, run the workflow [manually](https://github.com/akaunting/module-offline-payments/actions?query=workflow%3ATests) and make sure it passes.
|
||||
|
||||
## Translations
|
||||
|
||||
[Crowdin](https://crowdin.com/project/akaunting-apps) is the home of translators and it's synced (download & upload) with GitHub. The workflow is configured to run once per week and triggered manually. Therefore, **before** publishing a new release, run the workflow [manually](https://github.com/akaunting/module-offline-payments/actions?query=workflow%3ATranslations) and merge the automatically created PR, if available. Finally, all language files must be listed in the [config](https://github.com/akaunting/module-offline-payments/blob/master/crowdin.yml) file.
|
||||
120
modules/OfflinePayments/Resources/assets/js/offline-payments.js
vendored
Normal file
120
modules/OfflinePayments/Resources/assets/js/offline-payments.js
vendored
Normal file
@ -0,0 +1,120 @@
|
||||
/**
|
||||
* First we will load all of this project's JavaScript dependencies which
|
||||
* includes Vue and other libraries. It is a great starting point when
|
||||
* building robust, powerful web applications using Vue and Laravel.
|
||||
*/
|
||||
|
||||
require('./../../../../../resources/assets/js/bootstrap');
|
||||
|
||||
import Vue from 'vue';
|
||||
|
||||
import Global from './../../../../../resources/assets/js/mixins/global';
|
||||
import Form from './../../../../../resources/assets/js/plugins/form';
|
||||
import DashboardPlugin from './../../../../../resources/assets/js/plugins/dashboard-plugin';
|
||||
|
||||
// plugin setup
|
||||
Vue.use(DashboardPlugin);
|
||||
|
||||
const app = new Vue({
|
||||
el: '#app',
|
||||
|
||||
mixins: [
|
||||
Global
|
||||
],
|
||||
|
||||
data() {
|
||||
return {
|
||||
form: new Form('offline-payment'),
|
||||
update_code: null,
|
||||
form_loading: '',
|
||||
}
|
||||
},
|
||||
|
||||
methods:{
|
||||
onEdit(event) {
|
||||
axios.post(url + '/offline-payments/settings/get', {
|
||||
code: event
|
||||
})
|
||||
.then(response => {
|
||||
this.form.name = response.data.data.name;
|
||||
this.form.code = response.data.data.code;
|
||||
this.form.customer = response.data.data.customer;
|
||||
this.form.order = response.data.data.order;
|
||||
this.form.description = response.data.data.description;
|
||||
this.form.update_code = response.data.data.update_code;
|
||||
this.update_code = response.data.data.update_code;
|
||||
this.form.loading = false;
|
||||
this.form_loading = '';
|
||||
})
|
||||
.catch(error => {
|
||||
this.form.loading = false;
|
||||
this.form_loading = '';
|
||||
});
|
||||
},
|
||||
|
||||
// Actions > Delete
|
||||
confirmDelete(code, title, message, button_cancel, button_delete) {
|
||||
let confirm = {
|
||||
code: code,
|
||||
url: url,
|
||||
title: title,
|
||||
message: message,
|
||||
button_cancel: button_cancel,
|
||||
button_delete: button_delete,
|
||||
show: true
|
||||
};
|
||||
|
||||
this.component = Vue.component('add-new-component', (resolve, reject) => {
|
||||
resolve({
|
||||
template : '<div id="dynamic-delete-component"><akaunting-modal v-if="confirm.show" :show="confirm.show" :title="confirm.title" :message="confirm.message" :button_cancel="confirm.button_cancel" :button_delete="confirm.button_delete" @confirm="onDelete" @cancel="cancelDelete"></akaunting-modal></div>',
|
||||
|
||||
mixins: [
|
||||
Global
|
||||
],
|
||||
|
||||
data: function () {
|
||||
return {
|
||||
confirm: confirm,
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
// Delete action post
|
||||
async onDelete() {
|
||||
Promise.resolve(axios({
|
||||
method: 'DELETE',
|
||||
url: url + '/offline-payments/settings/delete',
|
||||
data: {
|
||||
code: this.confirm.code
|
||||
}
|
||||
})).then(response => {
|
||||
var type = (response.data.success) ? 'success' : 'warning';
|
||||
|
||||
if (response.data.success) {
|
||||
document.getElementById('method-' + this.confirm.code).remove();
|
||||
}
|
||||
|
||||
this.confirm.show = false;
|
||||
|
||||
this.$notify({
|
||||
message: response.data.message,
|
||||
timeout: 5000,
|
||||
icon: 'info',
|
||||
type
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
this.success = false;
|
||||
});
|
||||
},
|
||||
|
||||
// Close modal empty default value
|
||||
cancelDelete() {
|
||||
this.confirm.show = false;
|
||||
},
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
72
modules/OfflinePayments/Resources/assets/js/offline-payments.min.js
vendored
Normal file
72
modules/OfflinePayments/Resources/assets/js/offline-payments.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
21
modules/OfflinePayments/Resources/lang/ar-SA/general.php
Normal file
21
modules/OfflinePayments/Resources/lang/ar-SA/general.php
Normal file
@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
'name' => 'المدفوعات بدون الإتصال بالأنترنت',
|
||||
'description' => 'إنشاء خيارات دفع غير محدودة لاستخدام المسؤول',
|
||||
|
||||
'add_new' => 'إضافة جديد',
|
||||
'edit' => 'تعديل: :method',
|
||||
|
||||
'form' => [
|
||||
'code' => 'الكود',
|
||||
'customer' => 'إظهار للعميل',
|
||||
'order' => 'طلب'
|
||||
],
|
||||
|
||||
'methods' => 'طريقة | طرق',
|
||||
|
||||
'payment_gateways' => 'طرق الدفع بدون أنترنت',
|
||||
|
||||
];
|
||||
21
modules/OfflinePayments/Resources/lang/ca-ES/general.php
Normal file
21
modules/OfflinePayments/Resources/lang/ca-ES/general.php
Normal file
@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
'name' => 'Pagaments fora de línia',
|
||||
'description' => 'Crea opcions il·limitades de configuració de pagaments per que l\'administrador els utilitzi',
|
||||
|
||||
'add_new' => 'Afegeix-ne un de nou',
|
||||
'edit' => 'Edita :method',
|
||||
|
||||
'form' => [
|
||||
'code' => 'Codi',
|
||||
'customer' => 'Mostra a l\'usuari',
|
||||
'order' => 'Comanda'
|
||||
],
|
||||
|
||||
'methods' => 'Mètode|Mètodes',
|
||||
|
||||
'payment_gateways' => 'Mètodes de pagament fora de línia',
|
||||
|
||||
];
|
||||
21
modules/OfflinePayments/Resources/lang/da-DK/general.php
Normal file
21
modules/OfflinePayments/Resources/lang/da-DK/general.php
Normal file
@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
'name' => 'Offline betalinger',
|
||||
'description' => 'Opret ubegrænsede betalingsmuligheder til admin-brug',
|
||||
|
||||
'add_new' => 'Tilføj ny',
|
||||
'edit' => 'Rediger: :method',
|
||||
|
||||
'form' => [
|
||||
'code' => 'Kode',
|
||||
'customer' => 'Vis til kunde',
|
||||
'order' => 'Rækkefølge'
|
||||
],
|
||||
|
||||
'methods' => 'Metode|Metoder',
|
||||
|
||||
'payment_gateways' => 'Offline betalingsmetoder',
|
||||
|
||||
];
|
||||
21
modules/OfflinePayments/Resources/lang/de-DE/general.php
Normal file
21
modules/OfflinePayments/Resources/lang/de-DE/general.php
Normal file
@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
'name' => 'Offline-Zahlungen',
|
||||
'description' => 'Erstelle unbegrenzte Zahlungsoptionen für Adminnutzung',
|
||||
|
||||
'add_new' => 'Neu hinzufügen',
|
||||
'edit' => 'Bearbeiten: :method',
|
||||
|
||||
'form' => [
|
||||
'code' => 'Code',
|
||||
'customer' => 'Dem Kunden zeigen',
|
||||
'order' => 'Bestellung'
|
||||
],
|
||||
|
||||
'methods' => 'Methode|Methoden',
|
||||
|
||||
'payment_gateways' => 'Offline-Zahlungen-Methoden',
|
||||
|
||||
];
|
||||
22
modules/OfflinePayments/Resources/lang/en-GB/general.php
Normal file
22
modules/OfflinePayments/Resources/lang/en-GB/general.php
Normal file
@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
'name' => 'Offline Payments',
|
||||
'description' => 'Create unlimited payment options for admin usage',
|
||||
|
||||
'add_new' => 'Add New',
|
||||
'edit' => 'Edit: :method',
|
||||
|
||||
'form' => [
|
||||
'code' => 'Code',
|
||||
'customer' => 'Show to Customer',
|
||||
'order' => 'Order'
|
||||
],
|
||||
|
||||
'methods' => 'Method|Methods',
|
||||
|
||||
'payment_gateways' => 'Offline Payment Methods',
|
||||
'gateways_description' => 'Manage All Offline Payment Methods',
|
||||
|
||||
];
|
||||
21
modules/OfflinePayments/Resources/lang/es-ES/general.php
Normal file
21
modules/OfflinePayments/Resources/lang/es-ES/general.php
Normal file
@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
'name' => 'Pagos sin conexión',
|
||||
'description' => 'Crear opciones de pago ilimitadas para uso del administrador',
|
||||
|
||||
'add_new' => 'Añadir Nuevo',
|
||||
'edit' => 'Editar: :method',
|
||||
|
||||
'form' => [
|
||||
'code' => 'Código',
|
||||
'customer' => 'Mostrar Cliente',
|
||||
'order' => 'Pedido'
|
||||
],
|
||||
|
||||
'methods' => 'Método|Métodos',
|
||||
|
||||
'payment_gateways' => 'Asignación de Medios de Pago',
|
||||
|
||||
];
|
||||
21
modules/OfflinePayments/Resources/lang/fa-IR/general.php
Normal file
21
modules/OfflinePayments/Resources/lang/fa-IR/general.php
Normal file
@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
'name' => 'پرداخت های آفلاین',
|
||||
'description' => 'ساخت گزینه پرداخت نامحدود برای استفاده مدیر',
|
||||
|
||||
'add_new' => 'افزودن جدید',
|
||||
'edit' => 'ویرایش: :method',
|
||||
|
||||
'form' => [
|
||||
'code' => 'کد',
|
||||
'customer' => 'نمایش به مشتری',
|
||||
'order' => 'سفارش'
|
||||
],
|
||||
|
||||
'methods' => 'روش|روش ها',
|
||||
|
||||
'payment_gateways' => 'روش های پرداخت آفلاین',
|
||||
|
||||
];
|
||||
21
modules/OfflinePayments/Resources/lang/fr-FR/general.php
Normal file
21
modules/OfflinePayments/Resources/lang/fr-FR/general.php
Normal file
@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
'name' => 'Paiements hors-ligne',
|
||||
'description' => 'Créez un nombre illimité d\'options de paiement à usage administratif',
|
||||
|
||||
'add_new' => 'Ajouter un nouveau',
|
||||
'edit' => 'Modifier : :method',
|
||||
|
||||
'form' => [
|
||||
'code' => 'Code',
|
||||
'customer' => 'Montrer au client',
|
||||
'order' => 'Ordre'
|
||||
],
|
||||
|
||||
'methods' => 'Méthode|Méthodes',
|
||||
|
||||
'payment_gateways' => 'Méthodes de paiement hors ligne',
|
||||
|
||||
];
|
||||
21
modules/OfflinePayments/Resources/lang/id-ID/general.php
Normal file
21
modules/OfflinePayments/Resources/lang/id-ID/general.php
Normal file
@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
'name' => 'Pembayaran Offline',
|
||||
'description' => 'Buat opsi pembayaran tak terbatas untuk admin gunakan',
|
||||
|
||||
'add_new' => 'Tambah Baru',
|
||||
'edit' => 'Edit:: metode',
|
||||
|
||||
'form' => [
|
||||
'code' => 'Kode',
|
||||
'customer' => 'Tunjukkan kepada Pelanggan',
|
||||
'order' => 'Pesanan'
|
||||
],
|
||||
|
||||
'methods' => 'Metode|Metode',
|
||||
|
||||
'payment_gateways' => 'Metode Pembayaran Offline',
|
||||
|
||||
];
|
||||
21
modules/OfflinePayments/Resources/lang/it-IT/general.php
Normal file
21
modules/OfflinePayments/Resources/lang/it-IT/general.php
Normal file
@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
'name' => 'Pagamenti offline',
|
||||
'description' => 'Crea opzioni di pagamento illimitate per l\'utilizzo dell\'amministratore',
|
||||
|
||||
'add_new' => 'Aggiungi nuovo',
|
||||
'edit' => 'Modifica: :method',
|
||||
|
||||
'form' => [
|
||||
'code' => 'Codice',
|
||||
'customer' => 'Mostra al Cliente',
|
||||
'order' => 'Ordine'
|
||||
],
|
||||
|
||||
'methods' => 'Metodi',
|
||||
|
||||
'payment_gateways' => 'Metodi di pagamento offline',
|
||||
|
||||
];
|
||||
21
modules/OfflinePayments/Resources/lang/ja-JP/general.php
Normal file
21
modules/OfflinePayments/Resources/lang/ja-JP/general.php
Normal file
@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
'name' => 'オフライン支払い',
|
||||
'description' => '管理者が使用するための無制限の支払いオプションを作成する',
|
||||
|
||||
'add_new' => '新しく追加する',
|
||||
'edit' => 'エディット: :方法',
|
||||
|
||||
'form' => [
|
||||
'code' => 'コード',
|
||||
'customer' => '顧客に示す',
|
||||
'order' => 'オーダー'
|
||||
],
|
||||
|
||||
'methods' => '方法|方法',
|
||||
|
||||
'payment_gateways' => 'オフライン支払い方法',
|
||||
|
||||
];
|
||||
21
modules/OfflinePayments/Resources/lang/nb-NO/general.php
Normal file
21
modules/OfflinePayments/Resources/lang/nb-NO/general.php
Normal file
@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
'name' => 'Offline betalinger',
|
||||
'description' => 'Lag ubegrensede betalingsalternativer for administrasjonsbruk',
|
||||
|
||||
'add_new' => 'Legg til ny',
|
||||
'edit' => 'Rediger:: method',
|
||||
|
||||
'form' => [
|
||||
'code' => 'Kode',
|
||||
'customer' => 'Vis til kunde',
|
||||
'order' => 'Rekkefølge'
|
||||
],
|
||||
|
||||
'methods' => 'Metode|Metoder',
|
||||
|
||||
'payment_gateways' => 'Offline betalingsmetoder',
|
||||
|
||||
];
|
||||
21
modules/OfflinePayments/Resources/lang/pt-BR/general.php
Normal file
21
modules/OfflinePayments/Resources/lang/pt-BR/general.php
Normal file
@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
'name' => 'Pagamentos Offline',
|
||||
'description' => 'Crie opções de pagamento ilimitadas para uso do administrador',
|
||||
|
||||
'add_new' => 'Adicionar Novo',
|
||||
'edit' => 'Editar: :method',
|
||||
|
||||
'form' => [
|
||||
'code' => 'Código',
|
||||
'customer' => 'Mostrar ao Cliente',
|
||||
'order' => 'Ordem'
|
||||
],
|
||||
|
||||
'methods' => 'Método|Métodos',
|
||||
|
||||
'payment_gateways' => 'Métodos de Pagamento Offline',
|
||||
|
||||
];
|
||||
21
modules/OfflinePayments/Resources/lang/sq-AL/general.php
Normal file
21
modules/OfflinePayments/Resources/lang/sq-AL/general.php
Normal file
@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
'name' => 'Offline Payments',
|
||||
'description' => 'Krijoni mundësi të pakufizuara pagese për përdorim të administratorit',
|
||||
|
||||
'add_new' => 'Shto të Re',
|
||||
'edit' => 'Redaktoni: :method',
|
||||
|
||||
'form' => [
|
||||
'code' => 'Kodi',
|
||||
'customer' => 'Trego tek Klienti',
|
||||
'order' => 'Porosi'
|
||||
],
|
||||
|
||||
'methods' => 'Metodë|Metodat',
|
||||
|
||||
'payment_gateways' => 'Metodat e Pagesës Offline',
|
||||
|
||||
];
|
||||
21
modules/OfflinePayments/Resources/lang/uk-UA/general.php
Normal file
21
modules/OfflinePayments/Resources/lang/uk-UA/general.php
Normal file
@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
'name' => 'Оффлайн платежі',
|
||||
'description' => 'Створити необмежену кількість варіантів платежів для використання адміна',
|
||||
|
||||
'add_new' => 'Добавити нове',
|
||||
'edit' => 'Редагувати: :method',
|
||||
|
||||
'form' => [
|
||||
'code' => 'Код',
|
||||
'customer' => 'Показати Клієнту',
|
||||
'order' => 'Замовлення'
|
||||
],
|
||||
|
||||
'methods' => 'Метод|Методи',
|
||||
|
||||
'payment_gateways' => 'Способи Оффлайн Платежів',
|
||||
|
||||
];
|
||||
@ -0,0 +1,7 @@
|
||||
<h2>{{ $setting['name'] }}</h2>
|
||||
|
||||
@if ($setting['description'])
|
||||
<div class="well well-sm mt-2 blockquote">
|
||||
{{ $setting['description'] }}
|
||||
</div>
|
||||
@endif
|
||||
124
modules/OfflinePayments/Resources/views/edit.blade.php
Normal file
124
modules/OfflinePayments/Resources/views/edit.blade.php
Normal file
@ -0,0 +1,124 @@
|
||||
<x-layouts.admin>
|
||||
<x-slot name="title">{{ trans('offline-payments::general.name') }}</x-slot>
|
||||
|
||||
<x-slot name="favorite"
|
||||
title="{{ trans('offline-payments::general.name') }}"
|
||||
icon="credit_card"
|
||||
route="offline-payments.settings.edit"
|
||||
></x-slot>
|
||||
|
||||
<x-slot name="content">
|
||||
<x-show.container>
|
||||
<x-show.content class="flex flex-col-reverse lg:flex-row mt-5 sm:mt-12 gap-y-4" override="class">
|
||||
<x-show.content.left>
|
||||
<x-form id="offline-payment" method="POST" route="offline-payments.settings.update">
|
||||
<x-form.section>
|
||||
<x-slot name="head">
|
||||
<x-form.section.head title="{{ trans('general.add_new') }}" description="{{ trans('offline-payments::general.description') }}" />
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="body">
|
||||
<x-form.group.text name="name" label="{{ trans('general.name') }}" form-group-class="sm:col-span-6" />
|
||||
|
||||
<x-form.group.text name="order" label="{{ trans('offline-payments::general.form.order') }}" form-group-class="sm:col-span-6" not-required />
|
||||
|
||||
<x-form.group.textarea name="description" label="{{ trans('general.description') }}" form-group-class="sm:col-span-6" row="3" enable-v-model="true" not-required />
|
||||
|
||||
<x-form.group.toggle name="customer" label="{{ trans('offline-payments::general.form.customer') }}" :value="false" />
|
||||
|
||||
<x-form.input.hidden name="update_code" />
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="foot">
|
||||
<x-form.buttons :cancel="url()->previous()" />
|
||||
</x-slot>
|
||||
</x-form.section>
|
||||
</x-form>
|
||||
</x-show.content.left>
|
||||
|
||||
<x-show.content.right>
|
||||
<x-form.section.head title="{{ trans('offline-payments::general.payment_gateways') }}" description="{{ trans('offline-payments::general.gateways_description') }}" />
|
||||
|
||||
<x-table>
|
||||
<x-table.thead>
|
||||
<x-table.tr class="flex items-center px-1">
|
||||
<x-table.th class="w-3/12 sm:w-3/12">
|
||||
{{ trans('general.name') }}
|
||||
</x-table.th>
|
||||
|
||||
<x-table.th class="w-4/12 sm:w-5/12">
|
||||
{{ trans('general.description') }}
|
||||
</x-table.th>
|
||||
|
||||
<x-table.th class="w-2/12 sm:w-2/12">
|
||||
{{ trans('offline-payments::general.form.order') }}
|
||||
</x-table.th>
|
||||
|
||||
<x-table.th class="w-3/12 sm:w-2/12">
|
||||
{{ trans('general.actions') }}
|
||||
</x-table.th>
|
||||
</x-table.tr>
|
||||
</x-table.thead>
|
||||
|
||||
<x-table.tbody>
|
||||
@foreach($methods as $item)
|
||||
<x-table.tr id="method-{{ $item->code }}">
|
||||
<x-table.th class="w-3/12 sm:w-3/12">
|
||||
{{ $item->name }}
|
||||
</x-table.th>
|
||||
|
||||
<x-table.th class="w-4/12 sm:w-5/12">
|
||||
{{ $item->description ?? trans('general.na') }}
|
||||
</x-table.th>
|
||||
|
||||
<x-table.th class="w-2/12 sm:w-2/12">
|
||||
{{ $item->order }}
|
||||
</x-table.th>
|
||||
|
||||
<x-table.th class="w-3/12 sm:w-2/12">
|
||||
<div class="ltr:right-8 rtl:left-8 flex items-center">
|
||||
@can('update-offline-payments-settings')
|
||||
<x-button
|
||||
type="button"
|
||||
id="edit-{{ $item->code }}"
|
||||
data-code="{{ $item->code }}"
|
||||
class="relative bg-white hover:bg-gray-100 border py-0.5 px-1 cursor-pointer index-actions group"
|
||||
override="class"
|
||||
@click="onEdit('{{ $item->code }}')">
|
||||
<span class="material-icons-outlined text-purple text-lg">edit</span>
|
||||
<div class="inline-block absolute invisible z-20 py-1 px-2 text-sm font-medium text-gray-900 bg-white rounded-lg border border-gray-200 shadow-sm opacity-0 tooltip-content -top-10 -left-2" data-tooltip-placement="top">
|
||||
<span>{{ trans('general.edit') }}</span>
|
||||
<div class="absolute w-2 h-2 -bottom-1 before:content-[' '] before:absolute before:w-2 before:h-2 before:bg-white before:border-gray-200 before:transform before:rotate-45 before:border before:border-t-0 before:border-l-0" data-popper-arrow></div>
|
||||
</div>
|
||||
</x-button>
|
||||
@endcan
|
||||
|
||||
@can('delete-offline-payments-settings')
|
||||
<x-button
|
||||
type="button"
|
||||
class="relative bg-white hover:bg-gray-100 border py-0.5 px-1 cursor-pointer index-actions"
|
||||
override="class"
|
||||
id="delete-{{ $item->code }}"
|
||||
data-code="{{ $item->code }}"
|
||||
v-bind:disabled="update_code === '{{ $item->code }}'"
|
||||
@click="confirmDelete('{{ $item->code }}', '{{ trans('general.delete') }}', '{{ trans('general.delete_confirm', ['name' => '<strong>' . $item->name . '</strong>', 'type' => mb_strtolower(trans('offline-payments::general.name'))]) }}', '{{ trans('general.cancel') }}', '{{ trans('general.delete') }}')">
|
||||
<span class="material-icons-outlined text-purple text-lg">delete</span>
|
||||
<div class="inline-block absolute invisible z-20 py-1 px-2 text-sm font-medium text-gray-900 bg-white rounded-lg border border-gray-200 shadow-sm opacity-0 tooltip-content -top-10 -left-2" data-tooltip-placement="top">
|
||||
<span>{{ trans('general.delete') }}</span>
|
||||
<div class="absolute w-2 h-2 -bottom-1 before:content-[' '] before:absolute before:w-2 before:h-2 before:bg-white before:border-gray-200 before:transform before:rotate-45 before:border before:border-t-0 before:border-l-0" data-popper-arrow></div>
|
||||
</div>
|
||||
</x-button>
|
||||
@endcan
|
||||
</div>
|
||||
</x-table.th>
|
||||
</x-table.tr>
|
||||
@endforeach
|
||||
</x-table.tbody>
|
||||
</x-table>
|
||||
</x-show.content.right>
|
||||
</x-show.content>
|
||||
</x-show.container>
|
||||
</x-slot>
|
||||
|
||||
<x-script alias="offline-payments" file="offline-payments" />
|
||||
</x-layouts.admin>
|
||||
36
modules/OfflinePayments/Resources/views/show.blade.php
Normal file
36
modules/OfflinePayments/Resources/views/show.blade.php
Normal file
@ -0,0 +1,36 @@
|
||||
<div>
|
||||
<div>
|
||||
@if (!empty($setting['name']))
|
||||
<h2>{{ $setting['name'] }}</h2>
|
||||
@endif
|
||||
|
||||
@if (!empty($setting['description']))
|
||||
<div class="well well-sm mt-2 blockquote">{{ $setting['description'] }}</div>
|
||||
@endif
|
||||
</div>
|
||||
<br>
|
||||
|
||||
<div class="buttons">
|
||||
<div class="pull-right">
|
||||
<x-form id="redirect-form" method="POST" :url="$confirm_url">
|
||||
<x-button
|
||||
id="button-confirm"
|
||||
type="submit"
|
||||
@click="onRedirectConfirm"
|
||||
class="relative flex items-center justify-center bg-green hover:bg-green-700 text-white px-6 py-1.5 text-base rounded-lg disabled:bg-green-100"
|
||||
::disabled="form.loading"
|
||||
override="class"
|
||||
>
|
||||
<i v-if="form.loading" class="submit-spin absolute w-3 h-3 rounded-full left-0 right-0 -top-3.5 m-auto"></i>
|
||||
<span :class="[{'opacity-0': form.loading}]">
|
||||
{{ trans('general.confirm') }}
|
||||
</span>
|
||||
</x-button>
|
||||
|
||||
<x-form.input.hidden name="payment_method" :value="$setting['code']" />
|
||||
|
||||
<x-form.input.hidden name="type" value="income" />
|
||||
</x-form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
18
modules/OfflinePayments/Routes/admin.php
Normal file
18
modules/OfflinePayments/Routes/admin.php
Normal file
@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
/**
|
||||
* 'admin' middleware and 'offline-payments' prefix applied to all routes (including names)
|
||||
*
|
||||
* @see \App\Providers\Route::register
|
||||
*/
|
||||
|
||||
Route::admin('offline-payments', function () {
|
||||
Route::group(['prefix' => 'settings', 'as' => 'settings.'], function () {
|
||||
Route::get('/', 'Settings@edit')->name('edit');
|
||||
Route::post('/', 'Settings@update')->name('update');
|
||||
Route::post('get', 'Settings@get')->name('get');
|
||||
Route::delete('delete', 'Settings@destroy')->name('delete');
|
||||
});
|
||||
});
|
||||
14
modules/OfflinePayments/Routes/portal.php
Normal file
14
modules/OfflinePayments/Routes/portal.php
Normal file
@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
/**
|
||||
* 'portal' middleware and 'portal/offline-payments' prefix applied to all routes (including names)
|
||||
*
|
||||
* @see \App\Providers\Route::register
|
||||
*/
|
||||
|
||||
Route::portal('offline-payments', function () {
|
||||
Route::get('invoices/{invoice}', 'Payment@show')->name('invoices.show');
|
||||
Route::post('invoices/{invoice}/confirm', 'Payment@confirm')->name('invoices.confirm');
|
||||
});
|
||||
14
modules/OfflinePayments/Routes/signed.php
Normal file
14
modules/OfflinePayments/Routes/signed.php
Normal file
@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
/**
|
||||
* 'signed' middleware and 'signed/offline-payments' prefix applied to all routes (including names)
|
||||
*
|
||||
* @see \App\Providers\Route::register
|
||||
*/
|
||||
|
||||
Route::signed('offline-payments', function () {
|
||||
Route::get('invoices/{invoice}', 'Payment@show')->name('invoices.show');
|
||||
Route::post('invoices/{invoice}/confirm', 'Payment@confirm')->name('invoices.confirm');
|
||||
});
|
||||
33
modules/OfflinePayments/Tests/Feature/PaymentsTest.php
Normal file
33
modules/OfflinePayments/Tests/Feature/PaymentsTest.php
Normal file
@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\OfflinePayments\Tests\Feature;
|
||||
|
||||
use Tests\Feature\PaymentTestCase;
|
||||
|
||||
class PaymentsTest extends PaymentTestCase
|
||||
{
|
||||
public $alias = 'offline-payments';
|
||||
|
||||
public $payment_request = [
|
||||
'payment_method' => 'offline-payments.cash.99',
|
||||
'type' => 'income',
|
||||
];
|
||||
|
||||
public $setting_request = [
|
||||
'name' => 'offline payments',
|
||||
'code' => 'offline-payments.cash.99',
|
||||
'customer' => 1,
|
||||
'order' => 1,
|
||||
'description' => 'description',
|
||||
];
|
||||
|
||||
public function testItShouldPayFromSigned()
|
||||
{
|
||||
$this->assertPaymentFromSigned();
|
||||
}
|
||||
|
||||
public function testItShouldPayFromPortal()
|
||||
{
|
||||
$this->assertPaymentFromPortal();
|
||||
}
|
||||
}
|
||||
102
modules/OfflinePayments/Tests/Feature/SettingsTest.php
Normal file
102
modules/OfflinePayments/Tests/Feature/SettingsTest.php
Normal file
@ -0,0 +1,102 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\OfflinePayments\Tests\Feature;
|
||||
|
||||
use Modules\OfflinePayments\Jobs\CreatePaymentMethod;
|
||||
use Tests\Feature\FeatureTestCase;
|
||||
|
||||
class SettingsTest extends FeatureTestCase
|
||||
{
|
||||
public function testItShouldSeeOfflinePaymentsSettingsListPage()
|
||||
{
|
||||
$this->loginAs()
|
||||
->get(route('offline-payments.settings.edit'))
|
||||
->assertOk()
|
||||
->assertSeeText(trans('offline-payments::general.payment_gateways'))
|
||||
->assertSeeText(trans('offline-payments::general.description'));
|
||||
}
|
||||
|
||||
public function testItShouldSeeOfflinePaymentsSettingsCreatePage()
|
||||
{
|
||||
$this->loginAs()
|
||||
->get(route('offline-payments.settings.edit'))
|
||||
->assertOk()
|
||||
->assertSeeText(trans('offline-payments::general.add_new'));
|
||||
}
|
||||
|
||||
public function testItShouldCreateOfflinePaymentsSettings()
|
||||
{
|
||||
$request = $this->getRequest();
|
||||
|
||||
$message = trans('messages.success.added', ['type' => $request['name']]);
|
||||
|
||||
$this->loginAs()
|
||||
->post(route('offline-payments.settings.update'), $request)
|
||||
->assertOk()
|
||||
->assertJson([
|
||||
'success' => true,
|
||||
'message' => $message,
|
||||
]);
|
||||
}
|
||||
|
||||
public function testItShouldSeeOfflinePaymentsSettingsUpdatePage()
|
||||
{
|
||||
$payment_method = $this->dispatch(new CreatePaymentMethod($this->getRequest()));
|
||||
|
||||
$this->loginAs()
|
||||
->post(route('offline-payments.settings.get', ['code' => $payment_method['code']]))
|
||||
->assertOk()
|
||||
->assertJson([
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'name' => $payment_method['name'],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function testItShouldUpdateOfflinePaymentsSettings()
|
||||
{
|
||||
$request = $this->getRequest();
|
||||
|
||||
$payment_method = $this->dispatch(new CreatePaymentMethod($request));
|
||||
|
||||
$request['name'] = $this->faker->text(10);
|
||||
$request['update_code'] = $payment_method['code'];
|
||||
|
||||
$message = trans('messages.success.updated', ['type' => $request['name']]);
|
||||
|
||||
$this->loginAs()
|
||||
->post(route('offline-payments.settings.update'), $request)
|
||||
->assertOk()
|
||||
->assertJson([
|
||||
'success' => true,
|
||||
'message' => $message,
|
||||
]);
|
||||
}
|
||||
|
||||
public function testItShouldDeleteAccount()
|
||||
{
|
||||
$payment_method = $this->dispatch(new CreatePaymentMethod($this->getRequest()));
|
||||
|
||||
$message = trans('messages.success.deleted', ['type' => $payment_method['name']]);
|
||||
|
||||
$this->loginAs()
|
||||
->delete(route('offline-payments.settings.delete', ['code' => $payment_method['code']]))
|
||||
->assertOk()
|
||||
->assertJson([
|
||||
'success' => true,
|
||||
'message' => $message,
|
||||
]);
|
||||
}
|
||||
|
||||
public function getRequest()
|
||||
{
|
||||
return [
|
||||
'name' => $this->faker->text(10),
|
||||
'code' => 'offline-payments.cash.99',
|
||||
'customer' => 1,
|
||||
'order' => 1,
|
||||
'description' => $this->faker->paragraph,
|
||||
];
|
||||
}
|
||||
}
|
||||
21
modules/OfflinePayments/composer.json
Normal file
21
modules/OfflinePayments/composer.json
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"config": {
|
||||
"autoloader-suffix": "offlinePayments"
|
||||
},
|
||||
"name": "akaunting/module-offline-payments",
|
||||
"description": "Offline Payments app for Akaunting",
|
||||
"homepage": "https://akaunting.com",
|
||||
"keywords": [
|
||||
"laravel",
|
||||
"accounting",
|
||||
"invoice",
|
||||
"akaunting",
|
||||
"offline-payments"
|
||||
],
|
||||
"license": "GPL-3.0+",
|
||||
"scripts": {
|
||||
"test": [
|
||||
"composer install --prefer-dist --no-interaction --no-scripts --no-suggest --no-progress --no-ansi"
|
||||
]
|
||||
}
|
||||
}
|
||||
14
modules/OfflinePayments/crowdin.yml
Normal file
14
modules/OfflinePayments/crowdin.yml
Normal file
@ -0,0 +1,14 @@
|
||||
"project_id_env": CROWDIN_PROJECT_ID
|
||||
"api_token_env": CROWDIN_PERSONAL_TOKEN
|
||||
"preserve_hierarchy": true
|
||||
|
||||
"files": [
|
||||
{
|
||||
"source": "Resources/lang/en-GB/general.php",
|
||||
"dest": "offline-payments/general.php",
|
||||
"translation": "Resources/lang/%locale%/offline-payments/general.php",
|
||||
"translation_replace": {
|
||||
"offline-payments/": ""
|
||||
}
|
||||
}
|
||||
]
|
||||
3
modules/OfflinePayments/mix-manifest.json
Normal file
3
modules/OfflinePayments/mix-manifest.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"/Resources/assets/js/offline-payments.min.js": "/Resources/assets/js/offline-payments.min.js"
|
||||
}
|
||||
20
modules/OfflinePayments/module.json
Normal file
20
modules/OfflinePayments/module.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"alias": "offline-payments",
|
||||
"icon": "credit_card",
|
||||
"version": "3.0.4",
|
||||
"active": 1,
|
||||
"providers": [
|
||||
"Modules\\OfflinePayments\\Providers\\Event",
|
||||
"Modules\\OfflinePayments\\Providers\\Main"
|
||||
],
|
||||
"aliases": {},
|
||||
"files": [],
|
||||
"requires": [],
|
||||
"reports": [],
|
||||
"widgets": [],
|
||||
"settings": [],
|
||||
"extra-modules": {},
|
||||
"routes": {
|
||||
"redirect_after_install": "offline-payments.settings.edit"
|
||||
}
|
||||
}
|
||||
15954
modules/OfflinePayments/package-lock.json
generated
Normal file
15954
modules/OfflinePayments/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
21
modules/OfflinePayments/package.json
Normal file
21
modules/OfflinePayments/package.json
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "module-offline-payments",
|
||||
"version": "3.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "npm run development",
|
||||
"development": "mix",
|
||||
"watch": "mix watch",
|
||||
"watch-poll": "mix watch -- --watch-options-poll=1000",
|
||||
"hot": "mix watch --hot",
|
||||
"prod": "npm run production",
|
||||
"production": "mix --production"
|
||||
},
|
||||
"devDependencies": {
|
||||
"cross-env": "^5.2.1",
|
||||
"laravel-mix": "^6.0.39",
|
||||
"resolve-url-loader": "^5.0.0"
|
||||
},
|
||||
"author": "Akaunting Inc <info@akaunting.com>",
|
||||
"description": "Free Accounting Software"
|
||||
}
|
||||
22
modules/OfflinePayments/webpack.mix.js
vendored
Normal file
22
modules/OfflinePayments/webpack.mix.js
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
const mix = require('laravel-mix');
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Mix Asset Management
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Mix provides a clean, fluent API for defining some Webpack build steps
|
||||
| for your Laravel application. By default, we are compiling the Sass
|
||||
| file for the application as well as bundling up all the JS files.
|
||||
|
|
||||
*/
|
||||
|
||||
//mix.js('resources/assets/js/views/**/*.js', 'public/js')
|
||||
|
||||
mix.options({
|
||||
terser: {
|
||||
extractComments: false,
|
||||
}
|
||||
})
|
||||
.js('Resources/assets/js/offline-payments.js', 'Resources/assets/js/offline-payments.min.js')
|
||||
.vue();
|
||||
292
modules/PaypalStandard/.github/workflows/releases.yml
vendored
Normal file
292
modules/PaypalStandard/.github/workflows/releases.yml
vendored
Normal file
@ -0,0 +1,292 @@
|
||||
name: Releases
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [released]
|
||||
|
||||
jobs:
|
||||
check-json-version:
|
||||
|
||||
runs-on: ubuntu-20.04
|
||||
|
||||
steps:
|
||||
- name: Checkout module
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- id: set_var
|
||||
run: |
|
||||
content=`cat ./module.json`
|
||||
# the following lines are only required for multi line json
|
||||
content="${content//'%'/'%25'}"
|
||||
content="${content//$'\n'/'%0A'}"
|
||||
content="${content//$'\r'/'%0D'}"
|
||||
# end of optional handling for multi line json
|
||||
echo "::set-output name=moduleJson::$content"
|
||||
|
||||
- name: Set env
|
||||
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
|
||||
|
||||
- name: Check version equality
|
||||
run: |
|
||||
if [ "${{fromJson(steps.set_var.outputs.moduleJson).version}}" == $RELEASE_VERSION ]; then
|
||||
exit 0
|
||||
else
|
||||
exit 1
|
||||
fi
|
||||
|
||||
tests:
|
||||
|
||||
needs: [check-json-version]
|
||||
|
||||
name: PHP ${{ matrix.php }}
|
||||
|
||||
runs-on: ubuntu-20.04
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
php: ['8.1', '8.2']
|
||||
|
||||
steps:
|
||||
- name: Checkout Akaunting
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
repository: akaunting/akaunting
|
||||
|
||||
- name: Checkout module to tmp path
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
path: tmp-path-for-module
|
||||
|
||||
- id: set_var
|
||||
run: |
|
||||
content=`cat ./tmp-path-for-module/module.json`
|
||||
# the following lines are only required for multi line json
|
||||
content="${content//'%'/'%25'}"
|
||||
content="${content//$'\n'/'%0A'}"
|
||||
content="${content//$'\r'/'%0D'}"
|
||||
# end of optional handling for multi line json
|
||||
echo "::set-output name=moduleJson::$content"
|
||||
|
||||
- name: Create module folder name which is PascalCase
|
||||
run: |
|
||||
MODULE_FOLDER_NAME=$(echo -n ${{fromJson(steps.set_var.outputs.moduleJson).alias}} | sed -r 's/(^|-)([a-z])/\U\2/g')
|
||||
echo "MODULE_FOLDER_NAME=${MODULE_FOLDER_NAME}" >> $GITHUB_ENV
|
||||
|
||||
- name: Remove the tmp folder
|
||||
run: rm -rf tmp-path-for-module
|
||||
|
||||
- name: Checkout module
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
path: modules/${{ env.MODULE_FOLDER_NAME }}
|
||||
|
||||
- name: Cache Composer
|
||||
uses: actions/cache@v1
|
||||
with:
|
||||
path: ~/.composer/cache/files
|
||||
key: php-${{ matrix.php }}-composer-${{ hashFiles('composer.json') }}
|
||||
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: ${{ matrix.php }}
|
||||
extensions: bcmath, ctype, dom, fileinfo, intl, gd, json, mbstring, pdo, pdo_sqlite, openssl, sqlite, xml, zip
|
||||
coverage: none
|
||||
|
||||
- name: Copy .env
|
||||
run: cp .env.testing .env
|
||||
|
||||
- name: Install Composer
|
||||
run: cd "modules/${{ env.MODULE_FOLDER_NAME }}" ; composer test ; cd ../.. ; composer test
|
||||
|
||||
- name: Execute tests
|
||||
run: php artisan test --parallel
|
||||
|
||||
build:
|
||||
|
||||
needs: [tests]
|
||||
|
||||
runs-on: ubuntu-20.04
|
||||
|
||||
steps:
|
||||
- name: Checkout Akaunting
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
repository: akaunting/akaunting
|
||||
|
||||
- name: Checkout module to tmp path
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
path: tmp-path-for-module
|
||||
|
||||
- id: set_var
|
||||
run: |
|
||||
content=`cat ./tmp-path-for-module/module.json`
|
||||
# the following lines are only required for multi line json
|
||||
content="${content//'%'/'%25'}"
|
||||
content="${content//$'\n'/'%0A'}"
|
||||
content="${content//$'\r'/'%0D'}"
|
||||
# end of optional handling for multi line json
|
||||
echo "::set-output name=moduleJson::$content"
|
||||
|
||||
- name: Remove the tmp folder
|
||||
run: rm -rf tmp-path-for-module
|
||||
|
||||
- name: Create module folder name which is PascalCase
|
||||
run: |
|
||||
MODULE_FOLDER_NAME=$(echo -n ${{fromJson(steps.set_var.outputs.moduleJson).alias}} | sed -r 's/(^|-)([a-z])/\U\2/g')
|
||||
echo "MODULE_FOLDER_NAME=${MODULE_FOLDER_NAME}" >> $GITHUB_ENV
|
||||
|
||||
- name: Checkout module
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
path: modules/${{ env.MODULE_FOLDER_NAME }}
|
||||
|
||||
- name: Cache Composer
|
||||
uses: actions/cache@v1
|
||||
with:
|
||||
path: ~/.composer/cache/files
|
||||
key: php-8.1-composer-${{ hashFiles('composer.json') }}
|
||||
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: '8.1'
|
||||
extensions: bcmath, ctype, dom, fileinfo, intl, gd, json, mbstring, pdo, pdo_sqlite, openssl, sqlite, xml, zip
|
||||
coverage: none
|
||||
|
||||
- name: Copy .env
|
||||
run: cp .env.testing .env
|
||||
|
||||
- name: Install Composer
|
||||
run: cd "modules/${{ env.MODULE_FOLDER_NAME }}" ; composer install --no-dev ; cd ../.. ; composer install --no-dev
|
||||
|
||||
- name: Run npm install for Akaunting
|
||||
run: npm install
|
||||
|
||||
- name: Run npm install for the module
|
||||
run: |
|
||||
|
||||
cd "modules/${{ env.MODULE_FOLDER_NAME }}"
|
||||
npm install
|
||||
|
||||
- name: Compile module assets
|
||||
run: |
|
||||
|
||||
|
||||
cd "modules/${{ env.MODULE_FOLDER_NAME }}"
|
||||
|
||||
npm run production
|
||||
- name: Install zip
|
||||
run: sudo apt install zip
|
||||
|
||||
- name: Delete sass folder
|
||||
run: |
|
||||
cd "modules/${{ env.MODULE_FOLDER_NAME }}/Resources/assets"
|
||||
rm -rf sass
|
||||
|
||||
- name: Delete mix-manifest.json file
|
||||
run: |
|
||||
cd "modules/${{ env.MODULE_FOLDER_NAME }}"
|
||||
rm -f mix-manifest.json
|
||||
|
||||
- name: Delete non .min.js files
|
||||
run: |
|
||||
cd "modules/${{ env.MODULE_FOLDER_NAME }}/Resources/assets/js"
|
||||
find . -type f ! -name '*.min.js' -delete
|
||||
|
||||
- name: ZIP
|
||||
run: cd "modules/${{ env.MODULE_FOLDER_NAME }}" ; zip -r ../../final-build.zip . * -x '*.git*' -x '*node_modules*' -x 'composer.json' -x 'composer.lock' -x 'crowdin.yml' -x 'package-lock.json' -x 'package.json' -x 'README.md' -x 'webpack.mix.js' ; cd ..
|
||||
|
||||
- uses: actions/upload-artifact@master
|
||||
|
||||
with:
|
||||
name: final-build
|
||||
path: ${{ github.workspace }}/final-build.zip
|
||||
|
||||
upload-to-site:
|
||||
|
||||
needs: [build]
|
||||
|
||||
runs-on: ubuntu-20.04
|
||||
|
||||
steps:
|
||||
- uses: actions/download-artifact@master
|
||||
with:
|
||||
name: final-build
|
||||
|
||||
- name: Install unzip
|
||||
run: sudo apt install unzip
|
||||
|
||||
- name: UNZIP
|
||||
run: unzip final-build.zip -d src
|
||||
|
||||
- id: set_var
|
||||
run: |
|
||||
content=`cat ./src/module.json`
|
||||
# the following lines are only required for multi line json
|
||||
content="${content//'%'/'%25'}"
|
||||
content="${content//$'\n'/'%0A'}"
|
||||
content="${content//$'\r'/'%0D'}"
|
||||
# end of optional handling for multi line json
|
||||
echo "::set-output name=moduleJson::$content"
|
||||
|
||||
- name: Rename zip archive
|
||||
run: mv final-build.zip ${{fromJson(steps.set_var.outputs.moduleJson).alias}}-v${{fromJson(steps.set_var.outputs.moduleJson).version}}.zip
|
||||
|
||||
- name: Upload to release
|
||||
uses: svenstaro/upload-release-action@v2
|
||||
with:
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
file: ${{fromJson(steps.set_var.outputs.moduleJson).alias}}-v${{fromJson(steps.set_var.outputs.moduleJson).version}}.zip
|
||||
tag: ${{ github.ref }}
|
||||
overwrite: true
|
||||
|
||||
- name: Upload to Akaunting Site
|
||||
run: |
|
||||
curl \
|
||||
-H "Authorization: Bearer ${{ secrets.AKAUNTING_SITE_AUTH_BEARER }}" \
|
||||
|
||||
-H "Module-Version: ${{fromJson(steps.set_var.outputs.moduleJson).version}}" \
|
||||
-F "file=@./${{fromJson(steps.set_var.outputs.moduleJson).alias}}-v${{fromJson(steps.set_var.outputs.moduleJson).version}}.zip" \
|
||||
|
||||
--retry 5 \
|
||||
--retry-delay 60 \
|
||||
${{ secrets.AKAUNTING_SITE_API_DOMAIN }}/${{ secrets.AKAUNTING_SITE_API_MODULES_ENDPOINT }}/${{fromJson(steps.set_var.outputs.moduleJson).alias}}
|
||||
|
||||
update-cloud:
|
||||
|
||||
needs: [upload-to-site]
|
||||
|
||||
runs-on: ubuntu-20.04
|
||||
|
||||
steps:
|
||||
- uses: actions/download-artifact@master
|
||||
with:
|
||||
name: final-build
|
||||
|
||||
- name: Install unzip
|
||||
run: sudo apt install unzip
|
||||
|
||||
- name: UNZIP
|
||||
run: unzip final-build.zip -d src
|
||||
|
||||
- id: set_var
|
||||
run: |
|
||||
content=`cat ./src/module.json`
|
||||
# the following lines are only required for multi line json
|
||||
content="${content//'%'/'%25'}"
|
||||
content="${content//$'\n'/'%0A'}"
|
||||
content="${content//$'\r'/'%0D'}"
|
||||
# end of optional handling for multi line json
|
||||
echo "::set-output name=moduleJson::$content"
|
||||
|
||||
- name: Update the Cloud
|
||||
uses: appleboy/ssh-action@master
|
||||
with:
|
||||
host: ${{ secrets.AKAUNTING_CLOUD_HOST }}
|
||||
username: ${{ secrets.AKAUNTING_CLOUD_USERNAME }}
|
||||
key: ${{ secrets.AKAUNTING_CLOUD_SSH_PRIVATE }}
|
||||
script: |
|
||||
cd /${{ secrets.AKAUNTING_CLOUD_PATH }}
|
||||
${{ secrets.AKAUNTING_CLOUD_UPDATE_COMMAND }} ${{fromJson(steps.set_var.outputs.moduleJson).alias}} 1 ${{fromJson(steps.set_var.outputs.moduleJson).version}}
|
||||
55
modules/PaypalStandard/.github/workflows/tests.yml
vendored
Normal file
55
modules/PaypalStandard/.github/workflows/tests.yml
vendored
Normal file
@ -0,0 +1,55 @@
|
||||
name: Tests
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 0 * * 0'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
|
||||
name: PHP ${{ matrix.php }}
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
php: ['8.1', '8.2']
|
||||
|
||||
steps:
|
||||
- name: Checkout Akaunting
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
repository: akaunting/akaunting
|
||||
|
||||
- name: Checkout Paypal Standard module
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
path: modules/PaypalStandard
|
||||
|
||||
- name: Cache Composer
|
||||
|
||||
uses: actions/cache@v1
|
||||
with:
|
||||
path: ~/.composer/cache/files
|
||||
key: php-${{ matrix.php }}-composer-${{ hashFiles('composer.json') }}
|
||||
|
||||
- name: Setup PHP
|
||||
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: ${{ matrix.php }}
|
||||
extensions: bcmath, ctype, dom, fileinfo, intl, gd, json, mbstring, pdo, pdo_sqlite, openssl, sqlite, xml, zip
|
||||
coverage: none
|
||||
|
||||
- name: Copy .env
|
||||
|
||||
run: cp .env.testing .env
|
||||
|
||||
- name: Install Composer
|
||||
|
||||
run: cd modules/PaypalStandard ; composer test ; cd ../.. ; composer test
|
||||
|
||||
- name: Execute tests
|
||||
|
||||
run: php artisan test --parallel
|
||||
37
modules/PaypalStandard/.github/workflows/translations.yml
vendored
Normal file
37
modules/PaypalStandard/.github/workflows/translations.yml
vendored
Normal file
@ -0,0 +1,37 @@
|
||||
name: Translations
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 0 * * 0'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
sync:
|
||||
name: Sync
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Sync with Crowdin
|
||||
uses: crowdin/github-action@master
|
||||
with:
|
||||
upload_sources: true
|
||||
upload_translations: true
|
||||
download_translations: true
|
||||
skip_untranslated_files: true
|
||||
|
||||
localization_branch_name: 'translations'
|
||||
commit_message: 'new crowdin translations'
|
||||
pull_request_title: 'New Crowdin translations'
|
||||
pull_request_body: 'https://crowdin.com/project/akaunting-apps'
|
||||
pull_request_labels: 'Translation'
|
||||
|
||||
config: 'crowdin.yml'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_APPS_ID }}
|
||||
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
|
||||
7
modules/PaypalStandard/.gitignore
vendored
Normal file
7
modules/PaypalStandard/.gitignore
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
/.idea
|
||||
/.history
|
||||
/.vscode
|
||||
/.vagrant
|
||||
/node_modules
|
||||
/storage/*.key
|
||||
/vendor
|
||||
172
modules/PaypalStandard/Http/Controllers/Payment.php
Normal file
172
modules/PaypalStandard/Http/Controllers/Payment.php
Normal file
@ -0,0 +1,172 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\PaypalStandard\Http\Controllers;
|
||||
|
||||
use App\Abstracts\Http\PaymentController;
|
||||
use App\Http\Requests\Portal\InvoicePayment as PaymentRequest;
|
||||
use App\Models\Document\Document;
|
||||
use GuzzleHttp\Client;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class Payment extends PaymentController
|
||||
{
|
||||
public $alias = 'paypal-standard';
|
||||
|
||||
public $type = 'redirect';
|
||||
|
||||
public function show(Document $invoice, PaymentRequest $request, $cards = [])
|
||||
{
|
||||
$setting = $this->setting;
|
||||
|
||||
$this->setContactFirstLastName($invoice);
|
||||
|
||||
$setting['action'] = 'https://www.paypal.com/cgi-bin/webscr';
|
||||
|
||||
if (isset($setting['mode']) && $setting['mode'] == 'sandbox') {
|
||||
$setting['action'] = 'https://www.sandbox.paypal.com/cgi-bin/webscr';
|
||||
}
|
||||
|
||||
$invoice_url = $this->getInvoiceUrl($invoice);
|
||||
$return_url = $this->getReturnUrl($invoice);
|
||||
$confirm_url = $this->getConfirmUrl($invoice);
|
||||
|
||||
return response()->json([
|
||||
'code' => $setting['code'],
|
||||
'name' => $setting['name'],
|
||||
'description' => trans('paypal-standard::general.description'),
|
||||
'redirect' => false,
|
||||
'html' => view('paypal-standard::show', compact('setting', 'invoice', 'invoice_url', 'return_url', 'confirm_url'))->render(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the PayPal return redirect.
|
||||
*
|
||||
* Security (CWE-345): The return route is a user-facing redirect URL. It
|
||||
* must NOT mutate financial state (create transactions, mark invoices as
|
||||
* paid) because it trusts client-supplied POST data. Laravel's signed URL
|
||||
* validation only covers the URL path and query string, not the request
|
||||
* body — an attacker can POST `payment_status=Completed` to the signed
|
||||
* return URL without making a real PayPal payment.
|
||||
*
|
||||
* This method now only displays a "Thank You / Processing" screen by
|
||||
* redirecting to the finish page. The actual payment is processed
|
||||
* exclusively via the server-to-server `confirm` (IPN) callback, where
|
||||
* the transaction is cryptographically verified with PayPal.
|
||||
*
|
||||
* @param Document $invoice
|
||||
* @param Request $request
|
||||
* @return \Illuminate\Http\RedirectResponse
|
||||
*/
|
||||
public function return(Document $invoice, Request $request)
|
||||
{
|
||||
$message = trans('paypal-standard::general.payment.processing');
|
||||
|
||||
flash($message)->success();
|
||||
|
||||
return redirect($this->getFinishUrl($invoice));
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the PayPal IPN (Instant Payment Notification) callback.
|
||||
*
|
||||
* This is a server-to-server callback from PayPal. The notification is
|
||||
* verified by re-posting the data to PayPal and checking for a `VERIFIED`
|
||||
* response. Only after verification (and matching receiver email + amount)
|
||||
* is the `PaymentReceived` event dispatched, which creates the transaction
|
||||
* and marks the invoice as paid.
|
||||
*
|
||||
* @param Document $invoice
|
||||
* @param Request $request
|
||||
* @return void
|
||||
*/
|
||||
public function confirm(Document $invoice, Request $request)
|
||||
{
|
||||
$setting = $this->setting;
|
||||
|
||||
$paypal_log = $this->logger;
|
||||
|
||||
if (!$invoice) {
|
||||
return;
|
||||
}
|
||||
|
||||
$url = ($setting['mode'] == 'live') ? 'https://ipnpb.paypal.com/cgi-bin/webscr' : 'https://ipnpb.sandbox.paypal.com/cgi-bin/webscr';
|
||||
|
||||
$client = new Client(['verify' => false]);
|
||||
|
||||
$paypal_request['cmd'] = '_notify-validate';
|
||||
|
||||
foreach ($request->toArray() as $key => $value) {
|
||||
$paypal_request[$key] = urlencode(html_entity_decode($value, ENT_QUOTES, 'UTF-8'));
|
||||
}
|
||||
|
||||
try {
|
||||
$response = $client->post($url, $paypal_request);
|
||||
} catch (\Exception $e) {
|
||||
$paypal_log->info('PAYPAL_STANDARD :: CURL failed ' . $e->getMessage());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($response->getStatusCode() != 200) {
|
||||
$paypal_log->info('PAYPAL_STANDARD :: CURL failed ' . $response->getBody()->getContents());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$response_body = $response->getBody()->getContents();
|
||||
|
||||
if (!empty($setting['debug'])) {
|
||||
$paypal_log->info('PAYPAL_STANDARD :: IPN REQUEST: ', $request->toArray());
|
||||
$paypal_log->info('PAYPAL_STANDARD :: IPN RESPONSE: ' . $response_body);
|
||||
}
|
||||
|
||||
// Security: only accept PayPal's cryptographically verified response.
|
||||
// The previous condition used `||` which was always true (any string is
|
||||
// either not 'VERIFIED' OR not 'UNVERIFIED'), causing the method to
|
||||
// return early and never process any payment — defeating IPN entirely.
|
||||
if (strcmp($response_body, 'VERIFIED') !== 0) {
|
||||
$paypal_log->info('PAYPAL_STANDARD :: IPN NOT VERIFIED: ' . $response_body);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
switch ($request['payment_status']) {
|
||||
case 'Completed':
|
||||
$receiver_match = (strtolower($request['receiver_email']) == strtolower($setting['email']));
|
||||
|
||||
$total_paid_match = ((double) $request['mc_gross'] == $invoice->amount);
|
||||
|
||||
if ($receiver_match && $total_paid_match) {
|
||||
// Use PayPal's transaction ID as the reference.
|
||||
$this->setReference($invoice, $request['txn_id']);
|
||||
|
||||
$this->dispatchPaidEvent($invoice, $request->merge(['type' => 'income']));
|
||||
|
||||
$this->forgetReference($invoice);
|
||||
|
||||
$paypal_log->info('PAYPAL_STANDARD :: Payment Received for Invoice: ' . $invoice->id . ' - Txn ID: ' . $request['txn_id']);
|
||||
}
|
||||
|
||||
if (!$receiver_match) {
|
||||
$paypal_log->info('PAYPAL_STANDARD :: RECEIVER EMAIL MISMATCH! ' . strtolower($request['receiver_email']));
|
||||
}
|
||||
|
||||
if (!$total_paid_match) {
|
||||
$paypal_log->info('PAYPAL_STANDARD :: TOTAL PAID MISMATCH! ' . $request['mc_gross']);
|
||||
}
|
||||
break;
|
||||
case 'Canceled_Reversal':
|
||||
case 'Denied':
|
||||
case 'Expired':
|
||||
case 'Failed':
|
||||
case 'Pending':
|
||||
case 'Processed':
|
||||
case 'Refunded':
|
||||
case 'Reversed':
|
||||
case 'Voided':
|
||||
$paypal_log->info('PAYPAL_STANDARD :: NOT COMPLETED: ' . $request['payment_status']);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
19
modules/PaypalStandard/Listeners/AddLandingPage.php
Normal file
19
modules/PaypalStandard/Listeners/AddLandingPage.php
Normal file
@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\PaypalStandard\Listeners;
|
||||
|
||||
use App\Events\Auth\LandingPageShowing as Event;
|
||||
|
||||
class AddLandingPage
|
||||
{
|
||||
/**
|
||||
* Handle the event.
|
||||
*
|
||||
* @param Event $event
|
||||
* @return void
|
||||
*/
|
||||
public function handle(Event $event)
|
||||
{
|
||||
$event->user->landing_pages['paypal-standard.settings.edit'] = trans('paypal-standard::general.name');
|
||||
}
|
||||
}
|
||||
23
modules/PaypalStandard/Listeners/ShowAsPaymentMethod.php
Normal file
23
modules/PaypalStandard/Listeners/ShowAsPaymentMethod.php
Normal file
@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\PaypalStandard\Listeners;
|
||||
|
||||
use App\Events\Module\PaymentMethodShowing as Event;
|
||||
|
||||
class ShowAsPaymentMethod
|
||||
{
|
||||
/**
|
||||
* Handle the event.
|
||||
*
|
||||
* @param Event $event
|
||||
* @return void
|
||||
*/
|
||||
public function handle(Event $event)
|
||||
{
|
||||
$method = setting('paypal-standard');
|
||||
|
||||
$method['code'] = 'paypal-standard';
|
||||
|
||||
$event->modules->payment_methods[] = $method;
|
||||
}
|
||||
}
|
||||
30
modules/PaypalStandard/Providers/Event.php
Normal file
30
modules/PaypalStandard/Providers/Event.php
Normal file
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\PaypalStandard\Providers;
|
||||
|
||||
use Illuminate\Foundation\Support\Providers\EventServiceProvider as Provider;
|
||||
|
||||
class Event extends Provider
|
||||
{
|
||||
/**
|
||||
* Determine if events and listeners should be automatically discovered.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function shouldDiscoverEvents()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the listener directories that should be used to discover events.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function discoverEventsWithin()
|
||||
{
|
||||
return [
|
||||
__DIR__ . '/../Listeners',
|
||||
];
|
||||
}
|
||||
}
|
||||
81
modules/PaypalStandard/Providers/Main.php
Normal file
81
modules/PaypalStandard/Providers/Main.php
Normal file
@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\PaypalStandard\Providers;
|
||||
|
||||
use Illuminate\Support\ServiceProvider as Provider;
|
||||
|
||||
class Main extends Provider
|
||||
{
|
||||
/**
|
||||
* Boot the application events.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function boot()
|
||||
{
|
||||
$this->loadTranslations();
|
||||
$this->loadViews();
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the service provider.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register()
|
||||
{
|
||||
$this->loadRoutes();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load views.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function loadViews()
|
||||
{
|
||||
$this->loadViewsFrom(__DIR__ . '/../Resources/views', 'paypal-standard');
|
||||
}
|
||||
|
||||
/**
|
||||
* Load translations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function loadTranslations()
|
||||
{
|
||||
$this->loadTranslationsFrom(__DIR__ . '/../Resources/lang', 'paypal-standard');
|
||||
}
|
||||
|
||||
/**
|
||||
* Load routes.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function loadRoutes()
|
||||
{
|
||||
if (app()->routesAreCached()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$routes = [
|
||||
'guest.php',
|
||||
'portal.php',
|
||||
'signed.php',
|
||||
];
|
||||
|
||||
foreach ($routes as $route) {
|
||||
$this->loadRoutesFrom(__DIR__ . '/../Routes/' . $route);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the services provided by the provider.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function provides()
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
11
modules/PaypalStandard/README.md
Normal file
11
modules/PaypalStandard/README.md
Normal file
@ -0,0 +1,11 @@
|
||||
# PayPal Standard app for Akaunting
|
||||
|
||||
[](https://github.com/akaunting/module-paypal-standard/actions)
|
||||
|
||||
## Tests
|
||||
|
||||
The workflow runs both [Akaunting](https://github.com/akaunting/akaunting/tree/master/tests) and module test suites. They're configured to run once per week and triggered manually. Therefore, **before** publishing a new release, run the workflow [manually](https://github.com/akaunting/module-paypal-standard/actions?query=workflow%3ATests) and make sure it passes.
|
||||
|
||||
## Translations
|
||||
|
||||
[Crowdin](https://crowdin.com/project/akaunting-apps) is the home of translators and it's synced (download & upload) with GitHub. The workflow is configured to run once per week and triggered manually. Therefore, **before** publishing a new release, run the workflow [manually](https://github.com/akaunting/module-paypal-standard/actions?query=workflow%3ATranslations) and merge the automatically created PR, if available. Finally, all language files must be listed in the [config](https://github.com/akaunting/module-paypal-standard/blob/master/crowdin.yml) file.
|
||||
20
modules/PaypalStandard/Resources/lang/ar-SA/general.php
Normal file
20
modules/PaypalStandard/Resources/lang/ar-SA/general.php
Normal file
@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
'name' => 'معيار الباي بال',
|
||||
'description' => 'تمكين خيار الدفع القياسي لـ باي بال',
|
||||
|
||||
'form' => [
|
||||
'email' => 'البريد الإلكتروني',
|
||||
'mode' => 'نمط',
|
||||
'debug' => 'تصحيح الأخطاء',
|
||||
'transaction' => 'المعاملات',
|
||||
'customer' => 'إظهار للعميل',
|
||||
'order' => 'طلب',
|
||||
],
|
||||
|
||||
'test_mode' => 'تحذير: بوابة الدفع في وضع "Sandbox". لن يتم احتساب مبالغ على حسابك.',
|
||||
//'description' => 'Pay with PAYPAL',
|
||||
|
||||
];
|
||||
20
modules/PaypalStandard/Resources/lang/ca-ES/general.php
Normal file
20
modules/PaypalStandard/Resources/lang/ca-ES/general.php
Normal file
@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
'name' => 'PayPal estàndard',
|
||||
'description' => 'Activa el pagament estàndard amb PayPal',
|
||||
|
||||
'form' => [
|
||||
'email' => 'Correu electrònic',
|
||||
'mode' => 'Mode',
|
||||
'debug' => 'Depura',
|
||||
'transaction' => 'Transacció',
|
||||
'customer' => 'Mostra al client',
|
||||
'order' => 'Comanda',
|
||||
],
|
||||
|
||||
'test_mode' => 'Avís: La passarel·la de pagament està en \'mode de proves\'. El teu compte no es veurà afectat per l\'acció.',
|
||||
//'description' => 'Pay with PAYPAL',
|
||||
|
||||
];
|
||||
20
modules/PaypalStandard/Resources/lang/da-DK/general.php
Normal file
20
modules/PaypalStandard/Resources/lang/da-DK/general.php
Normal file
@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
'name' => 'PayPal Standard',
|
||||
'description' => 'Aktivér standard betalingsmulighed for PayPal',
|
||||
|
||||
'form' => [
|
||||
'email' => 'E-mail',
|
||||
'mode' => 'Mode',
|
||||
'debug' => 'Fejlfinde',
|
||||
'transaction' => 'Transaktion',
|
||||
'customer' => 'Vis til kunde',
|
||||
'order' => 'Rækkefølge',
|
||||
],
|
||||
|
||||
'test_mode' => 'Advarsel: Betalingsgatewayen er i \'Sandbox Mode\'. Din konto bliver ikke debiteret.',
|
||||
//'description' => 'Pay with PAYPAL',
|
||||
|
||||
];
|
||||
20
modules/PaypalStandard/Resources/lang/de-DE/general.php
Normal file
20
modules/PaypalStandard/Resources/lang/de-DE/general.php
Normal file
@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
'name' => 'PayPal Standard',
|
||||
'description' => 'Aktivieren Sie die Standard-Zahlungsoption von PayPal',
|
||||
|
||||
'form' => [
|
||||
'email' => 'E-Mail',
|
||||
'mode' => 'Modus',
|
||||
'debug' => 'Debug',
|
||||
'transaction' => 'Transaktion',
|
||||
'customer' => 'Dem Kunden zeigen',
|
||||
'order' => 'Bestellung',
|
||||
],
|
||||
|
||||
'test_mode' => 'Warnung: Das Payment-Gateway befindet sich im <b>Sandbox-Modus</b>. Ihr Konto wird nicht belastet.',
|
||||
//'description' => 'Pay with PAYPAL',
|
||||
|
||||
];
|
||||
26
modules/PaypalStandard/Resources/lang/en-GB/general.php
Normal file
26
modules/PaypalStandard/Resources/lang/en-GB/general.php
Normal file
@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
'name' => 'PayPal Standard',
|
||||
'description' => 'Enable the standard payment option of PayPal',
|
||||
|
||||
'form' => [
|
||||
'email' => 'Email',
|
||||
'mode' => 'Mode',
|
||||
'debug' => 'Debug',
|
||||
'transaction' => 'Transaction',
|
||||
'customer' => 'Show to Customer',
|
||||
'order' => 'Order',
|
||||
],
|
||||
|
||||
'payment' => [
|
||||
'pending' => 'Payment is pending',
|
||||
'not_added' => 'Payment not added!',
|
||||
'processing' => 'Thank you! Your payment is being processed. You will be notified once it is confirmed.',
|
||||
],
|
||||
|
||||
'test_mode' => 'Warning: The payment gateway is in \'Sandbox Mode\'. Your account will not be charged.',
|
||||
//'description' => 'Pay with PAYPAL',
|
||||
|
||||
];
|
||||
20
modules/PaypalStandard/Resources/lang/fa-IR/general.php
Normal file
20
modules/PaypalStandard/Resources/lang/fa-IR/general.php
Normal file
@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
'name' => 'PayPal استاندارد',
|
||||
'description' => 'گزینه پرداخت استاندارد PayPal را فعال کنید',
|
||||
|
||||
'form' => [
|
||||
'email' => 'ایمیل',
|
||||
'mode' => 'حالت',
|
||||
'debug' => 'اشکال زدايي کردن',
|
||||
'transaction' => 'تراکنشها',
|
||||
'customer' => 'نمایش به مشتری',
|
||||
'order' => 'سفارش',
|
||||
],
|
||||
|
||||
'test_mode' => 'هشدار: درگاه پرداخت در "حالت Sandbox" است. حساب شما شارژ نخواهد شد.',
|
||||
//'description' => 'Pay with PAYPAL',
|
||||
|
||||
];
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user