Skip to content

Commit

Permalink
Merge pull request #330 from liberu-billing/sweep/Implement-Billing-A…
Browse files Browse the repository at this point in the history
…PI-with-Authentication-Invoices-and-API-Documentation

Implement Billing API with Authentication, Invoices, and API Documentation
  • Loading branch information
curtisdelicata authored Dec 25, 2024
2 parents 015edaf + 846de1f commit ceaec3a
Show file tree
Hide file tree
Showing 5 changed files with 362 additions and 4 deletions.
50 changes: 50 additions & 0 deletions app/Http/Controllers/Api/AuthController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@


<?php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\ValidationException;

class AuthController extends Controller
{
public function token(Request $request)
{
$request->validate([
'email' => 'required|email',
'password' => 'required',
'device_name' => 'required',
]);

$user = User::where('email', $request->email)->first();

if (! $user || ! Hash::check($request->password, $user->password)) {
throw ValidationException::withMessages([
'email' => ['The provided credentials are incorrect.'],
]);
}

$token = $user->createToken($request->device_name);

return response()->json([
'token' => $token->plainTextToken,
'user' => $user,
]);
}

public function revokeToken(Request $request)
{
$request->user()->currentAccessToken()->delete();
return response()->json(['message' => 'Token revoked successfully']);
}

public function revokeAllTokens(Request $request)
{
$request->user()->tokens()->delete();
return response()->json(['message' => 'All tokens revoked successfully']);
}
}
87 changes: 87 additions & 0 deletions app/Http/Controllers/Api/InvoiceController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@


<?php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Models\Invoice;
use App\Http\Resources\Api\InvoiceResource;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\PDF;
use Symfony\Component\HttpFoundation\Response;

class InvoiceController extends Controller
{
public function index(Request $request)
{
$invoices = Invoice::query()
->when($request->status, fn($q) => $q->where('status', $request->status))
->when($request->customer_id, fn($q) => $q->where('customer_id', $request->customer_id))
->when($request->from_date, fn($q) => $q->where('issue_date', '>=', $request->from_date))
->when($request->to_date, fn($q) => $q->where('issue_date', '<=', $request->to_date))
->paginate($request->per_page ?? 15);

return InvoiceResource::collection($invoices);
}

public function show(Invoice $invoice)
{
return new InvoiceResource($invoice->load(['customer', 'items']));
}

public function store(Request $request)
{
$validated = $request->validate([
'customer_id' => 'required|exists:customers,id',
'issue_date' => 'required|date',
'due_date' => 'required|date|after_or_equal:issue_date',
'items' => 'required|array|min:1',
'items.*.description' => 'required|string',
'items.*.quantity' => 'required|numeric|min:1',
'items.*.price' => 'required|numeric|min:0',
]);

$invoice = Invoice::create($validated);
$invoice->items()->createMany($validated['items']);

return new InvoiceResource($invoice->load(['customer', 'items']));
}

public function update(Request $request, Invoice $invoice)
{
if ($invoice->status === 'paid') {
return response()->json([
'message' => 'Cannot update a paid invoice'
], Response::HTTP_UNPROCESSABLE_ENTITY);
}

$validated = $request->validate([
'issue_date' => 'sometimes|date',
'due_date' => 'sometimes|date|after_or_equal:issue_date',
'status' => 'sometimes|in:draft,sent,paid,cancelled',
]);

$invoice->update($validated);

return new InvoiceResource($invoice->load(['customer', 'items']));
}

public function destroy(Invoice $invoice)
{
if ($invoice->status !== 'draft') {
return response()->json([
'message' => 'Only draft invoices can be deleted'
], Response::HTTP_UNPROCESSABLE_ENTITY);
}

$invoice->delete();
return response()->noContent();
}

public function download(Invoice $invoice)
{
$pdf = PDF::loadView('invoices.pdf', ['invoice' => $invoice->load(['customer', 'items'])]);
return $pdf->download("invoice-{$invoice->invoice_number}.pdf");
}
}
38 changes: 38 additions & 0 deletions app/Http/Resources/Api/InvoiceResource.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@


<?php

namespace App\Http\Resources\Api;

use Illuminate\Http\Resources\Json\JsonResource;

class InvoiceResource extends JsonResource
{
public function toArray($request)
{
return [
'id' => $this->id,
'invoice_number' => $this->invoice_number,
'customer' => new CustomerResource($this->whenLoaded('customer')),
'total_amount' => $this->total_amount,
'currency' => $this->currency,
'status' => $this->status,
'issue_date' => $this->issue_date->toIso8601String(),
'due_date' => $this->due_date->toIso8601String(),
'paid_at' => $this->paid_at?->toIso8601String(),
'late_fee_amount' => $this->late_fee_amount,
'items' => InvoiceItemResource::collection($this->whenLoaded('items')),
'subtotal' => $this->subtotal,
'tax_amount' => $this->tax_amount,
'discount_amount' => $this->discount_amount,
'final_total' => $this->final_total,
'notes' => $this->notes,
'created_at' => $this->created_at->toIso8601String(),
'updated_at' => $this->updated_at->toIso8601String(),
'links' => [
'self' => route('api.invoices.show', $this->id),
'download' => route('api.invoices.download', $this->id),
],
];
}
}
159 changes: 159 additions & 0 deletions docs/api.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@


openapi: 3.0.0
info:
title: Billing API
version: 1.0.0
description: API for accessing billing data and managing invoices, subscriptions, and customers

servers:
- url: /api/v1

components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT

schemas:
Invoice:
type: object
properties:
id:
type: integer
invoice_number:
type: string
customer:
$ref: '#/components/schemas/Customer'
total_amount:
type: number
format: float
currency:
type: string
status:
type: string
enum: [draft, sent, paid, cancelled]
issue_date:
type: string
format: date-time
due_date:
type: string
format: date-time
paid_at:
type: string
format: date-time
nullable: true
items:
type: array
items:
$ref: '#/components/schemas/InvoiceItem'

paths:
/auth/token:
post:
summary: Generate API token
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
email:
type: string
password:
type: string
device_name:
type: string
responses:
200:
description: Token generated successfully
content:
application/json:
schema:
type: object
properties:
token:
type: string
user:
type: object

/invoices:
get:
summary: List invoices
security:
- bearerAuth: []
parameters:
- name: status
in: query
schema:
type: string
- name: customer_id
in: query
schema:
type: integer
- name: from_date
in: query
schema:
type: string
format: date
- name: to_date
in: query
schema:
type: string
format: date
responses:
200:
description: List of invoices
content:
application/json:
schema:
type: object
properties:
data:
type: array
items:
$ref: '#/components/schemas/Invoice'
links:
type: object
meta:
type: object

post:
summary: Create new invoice
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
customer_id:
type: integer
issue_date:
type: string
format: date
due_date:
type: string
format: date
items:
type: array
items:
type: object
properties:
description:
type: string
quantity:
type: number
price:
type: number
responses:
201:
description: Invoice created successfully
content:
application/json:
schema:
$ref: '#/components/schemas/Invoice'
32 changes: 28 additions & 4 deletions routes/api.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\Api\AuthController;
use App\Http\Controllers\Api\InvoiceController;
use App\Http\Controllers\Api\SubscriptionController;
use App\Http\Controllers\Api\CustomerController;
use App\Http\Controllers\ClientNoteController;

/*
Expand All @@ -15,11 +19,31 @@
|
*/

Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
return $request->user();
});
// Public routes
Route::post('auth/token', [AuthController::class, 'token']);

// Protected routes
Route::middleware(['auth:sanctum', 'throttle:api'])->group(function () {
// User endpoint
Route::get('/user', function (Request $request) {
return $request->user();
});

// Invoice endpoints
Route::apiResource('invoices', InvoiceController::class);
Route::get('invoices/{invoice}/download', [InvoiceController::class, 'download']);

// Subscription endpoints
Route::apiResource('subscriptions', SubscriptionController::class);
Route::post('subscriptions/{subscription}/cancel', [SubscriptionController::class, 'cancel']);
Route::post('subscriptions/{subscription}/renew', [SubscriptionController::class, 'renew']);

// Customer endpoints
Route::apiResource('customers', CustomerController::class);
Route::get('customers/{customer}/invoices', [CustomerController::class, 'invoices']);
Route::get('customers/{customer}/subscriptions', [CustomerController::class, 'subscriptions']);

Route::middleware('auth:sanctum')->group(function () {
// Client Notes endpoints
Route::get('client-notes', [ClientNoteController::class, 'index']);
Route::post('client-notes', [ClientNoteController::class, 'store']);
Route::delete('client-notes/{note}', [ClientNoteController::class, 'destroy']);
Expand Down

0 comments on commit ceaec3a

Please sign in to comment.