Skip to content

Commit

Permalink
feat: Implement midtrans online payment
Browse files Browse the repository at this point in the history
  • Loading branch information
bangyadiii committed May 20, 2024
1 parent 0fdd918 commit 481c245
Show file tree
Hide file tree
Showing 48 changed files with 2,027 additions and 102 deletions.
11 changes: 11 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379

TELESCOPE_ENABLED=false

MAIL_MAILER=smtp
MAIL_HOST=mailpit
MAIL_PORT=1025
Expand All @@ -53,3 +55,12 @@ PUSHER_APP_CLUSTER=mt1

MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"

WABLAS_TOKEN=
WABLAS_SERVER=

MIDTRANS_CLIENT_KEY=
MIDTRANS_SERVER_KEY=
MIDTRANS_SB_CLIENT_KEY=
MIDTRANS_SB_SERVER_KEY=
MIDTRANS_IS_PRODUCTION=
14 changes: 14 additions & 0 deletions IntelephenseHelper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

namespace Illuminate\Contracts\View;

use Illuminate\Contracts\Support\Renderable;

interface View extends Renderable
{
/** @return static */
public function extends(string $layout);
public function layoutData();
public function layout(string $layout);
//any other method that throws false error here :)
}
117 changes: 117 additions & 0 deletions app/Http/Controllers/MidtransCallbackController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
<?php

namespace App\Http\Controllers;

use App\Models\Bill;
use App\Models\Payment;
use App\Models\PaymentLog;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Sawirricardo\Midtrans\Dto\TransactionStatus;
use Sawirricardo\Midtrans\Laravel\Notification;
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;

class MidtransCallbackController extends Controller
{
public function receive(Request $request)
{
$notification = Notification::make($request->all());
$notification
->whenSettlement(function (TransactionStatus $notification) {
$this->updatePaymentStatus($notification, 'success');
})
->whenPending(function (TransactionStatus $notification) {
// create a new payment and payment log
$billId = explode('.', $notification->order_id)[1];
$bill = Bill::query()->findOrFail($billId);
$payment = Payment::query()->where('bill_id', $bill->id)
->first();

if (!$payment) {
$payment = Payment::create([
'bill_id' => $bill->id,
'amount' => $bill->total_amount,
'payment_date' => \now(),
'status' => 'pending',
'method' => 'midtrans',
]);
}

/**
* @var ?PaymentLog $log
*/
$log = PaymentLog::query()->where('payment_id', $payment->id)
->where('transaction_id', $notification->transaction_id)
->first();

if ($log) {
$log->touch();
return;
}

PaymentLog::create([
'payment_id' => $payment->id,
'transaction_id' => $notification->transaction_id,
'status_code' => $notification->status_code,
'log_message' => \json_encode($notification->toArray()),
]);
})
->whenDenied(function (TransactionStatus $notification) {
$this->updatePaymentStatus($notification, 'failed');
})
->whenExpired(function (TransactionStatus $notification) {
$this->updatePaymentStatus($notification, 'failed');
})
->whenCancelled(function (TransactionStatus $notification) {
$this->updatePaymentStatus($notification, 'failed');
})
->listen();


return response()->json(['status' => 'success', 'message' => 'Callback received']);
}

private function updatePaymentStatus(TransactionStatus $notification, string $status)
{
$billId = explode('.', $notification->order_id)[1];
$bill = Bill::query()->findOrFail($billId);
$payment = Payment::query()->where('bill_id', $bill->id)
->firstOrFail();

$serverKey = \config('midtrans.is_production') ?
\config('midtrans.server_key') : \config('midtrans.sandbox_server_key');

if (!$this->checkSignature($notification->signature_key, $notification->order_id, $notification->status_code, $notification->gross_amount, $serverKey)) {
throw new UnauthorizedHttpException('Unauthorized');
}

DB::transaction(function () use ($payment, $bill, $status, $notification) {
$payment->fill([
'status' => $status,
'payment_date' => now(),
])->save();

$billStatus = $status === 'success' ? 'paid' : 'unpaid';
$bill->fill(['status' => $billStatus])->save();

$log = PaymentLog::query()->updateOrCreate([
'payment_id' => $payment->id,
'transaction_id' => $notification->transaction_id,
], [
'status_code' => $notification->status_code,
'log_message' => \json_encode($notification->toArray()),
]);

\info('Payment log created', $log->toArray());
});
}

private function checkSignature(string $signature, string $orderId, string $statusCode, string $grossAmount, string $serverKey): bool
{
$expectedSignature = hash('sha512', $orderId . $statusCode . $grossAmount . $serverKey);
if ($signature !== $expectedSignature) {
return false;
}
return true;
}
}
3 changes: 2 additions & 1 deletion app/Livewire/Customer/CreateCustomer.php
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@ public function store()
$customer->bills()->create([
'due_date' => $isolirDate,
'plan_id' => $customer->plan_id,
'total_amount' => $plan->price,
'total_amount' => ($plan->price - $this->form->discount) * 1.11,
'tax_rate' => 11, // TODO: get tax rate from setting
'discount' => $this->form->discount,
'status' => 'unpaid',
]);
Expand Down
15 changes: 15 additions & 0 deletions app/Livewire/Forms/BillCheckForm.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

namespace App\Livewire\Forms;

use Livewire\Attributes\Validate;
use Livewire\Form;

class BillCheckForm extends Form
{
public $customer_id;

public array $rules = [
'customer_id' => 'required|exists:customers,id',
];
}
33 changes: 33 additions & 0 deletions app/Livewire/Forms/Transaction/OnlinePaymentForm.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

namespace App\Livewire\Forms\Transaction;

use App\Models\Customer;
use Livewire\Attributes\Validate;
use Livewire\Form;

class OnlinePaymentForm extends Form
{
public $id;
public $customer_name;
public $phone_number;
public $address;
public $plan_name;
public $plan_price;
public $billStatus = 'paid';

public function setCustomer(Customer $customer)
{
$this->id = $customer->id;
$this->customer_name = $customer->customer_name;
$this->phone_number = $customer->phone_number;
$this->address = $customer->address;
$this->plan_name = $customer->plan->name;
$this->plan_price = $customer->plan->price;
}

public function setBill($bill)
{
$this->billStatus = isset($bill) ? 'unpaid' : 'paid';
}
}
2 changes: 1 addition & 1 deletion app/Livewire/Forms/TransactionForm.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ class TransactionForm extends Form
public $discount;
public $method;
public $note;
public $tax_rate = 11;
public $tax_rate = 11; // TODO: move to database

public array $rules = [
'method' => 'required|string|max:255',
Expand Down
98 changes: 98 additions & 0 deletions app/Livewire/LoginComponent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
<?php

namespace App\Livewire;

use App\Providers\RouteServiceProvider;
use Livewire\Component;

use Illuminate\Auth\Events\Lockout;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;

class LoginComponent extends Component
{
public string $username = '';
public string $password = '';
public bool $remember = false;

public function render()
{
return view('livewire.login-component')
->layout('layouts.blankLayout');
}

/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\Rule|array|string>
*/
public function rules(): array
{
return [
'username' => ['required', 'string'],
'password' => ['required', 'string'],
'remember' => ['nullable', 'boolean'],
];
}

public function store()
{
$this->authenticate();
\request()->session()->regenerate();

return $this->redirectIntended(RouteServiceProvider::HOME, navigate: true);
}

/**
* Ensure the login request is not rate limited.
*
* @throws \Illuminate\Validation\ValidationException
*/
public function ensureIsNotRateLimited(): void
{
if (!RateLimiter::tooManyAttempts($this->throttleKey(), 5)) {
return;
}

event(new Lockout(request()));

$seconds = RateLimiter::availableIn($this->throttleKey());

throw ValidationException::withMessages([
'username' => trans('auth.throttle', [
'seconds' => $seconds,
'minutes' => ceil($seconds / 60),
]),
]);
}

/**
* Get the rate limiting throttle key for the request.
*/
public function throttleKey(): string
{
return Str::transliterate(Str::lower($this->username) . '|' . request()->ip());
}

/**
* Attempt to authenticate the request's credentials.
*
* @throws \Illuminate\Validation\ValidationException
*/
public function authenticate(): void
{
$this->ensureIsNotRateLimited();

if (!Auth::attempt($this->only('username', 'password'), $this->remember)) {
RateLimiter::hit($this->throttleKey());

throw ValidationException::withMessages([
'username' => trans('auth.failed'),
]);
}

RateLimiter::clear($this->throttleKey());
}
}
25 changes: 25 additions & 0 deletions app/Livewire/LogoutComponent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

namespace App\Livewire;

use Illuminate\Support\Facades\Auth;
use Livewire\Component;

class LogoutComponent extends Component
{
public function render()
{
return view('livewire.logout-component');
}

public function logout()
{
Auth::guard('web')->logout();

request()->session()->invalidate();

request()->session()->regenerateToken();

return $this->redirectRoute('login');
}
}
24 changes: 24 additions & 0 deletions app/Livewire/Transaction/BillCheck.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

namespace App\Livewire\Transaction;

use App\Livewire\Forms\BillCheckForm;
use App\Models\Customer;
use Livewire\Component;

class BillCheck extends Component
{
public BillCheckForm $form;

public function render()
{
return view('livewire.transaction.bill-check')
->layout('layouts.blankLayout');
}

public function store()
{
$this->validate();
return $this->redirectIntended("payment/" . $this->form->customer_id, navigate: true);
}
}
Loading

0 comments on commit 481c245

Please sign in to comment.