Skip to content

Commit

Permalink
Add Invoice Dispute Management Feature
Browse files Browse the repository at this point in the history
  • Loading branch information
sweep-ai[bot] authored Dec 25, 2024
1 parent e640d10 commit 8d32534
Show file tree
Hide file tree
Showing 5 changed files with 246 additions and 0 deletions.
60 changes: 60 additions & 0 deletions app/Http/Controllers/InvoiceDisputeController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@


<?php

namespace App\Http\Controllers;

use App\Models\Invoice;
use App\Models\InvoiceDispute;
use App\Services\DisputeService;
use Illuminate\Http\Request;

class InvoiceDisputeController extends Controller
{
protected $disputeService;

public function __construct(DisputeService $disputeService)
{
$this->disputeService = $disputeService;
}

public function store(Request $request, Invoice $invoice)
{
$validated = $request->validate([
'reason' => 'required|string|max:255',
'description' => 'required|string'
]);

$dispute = $this->disputeService->createDispute($invoice, $validated);

return response()->json($dispute, 201);
}

public function update(Request $request, InvoiceDispute $dispute)
{
$validated = $request->validate([
'status' => 'required|in:under_review,resolved,rejected',
'resolution_notes' => 'required_if:status,resolved,rejected|string'
]);

$dispute = $this->disputeService->updateDisputeStatus(
$dispute,
$validated['status'],
$validated['resolution_notes'] ?? null
);

return response()->json($dispute);
}

public function addMessage(Request $request, InvoiceDispute $dispute)
{
$validated = $request->validate([
'message' => 'required|string',
'attachments' => 'nullable|array'
]);

$message = $this->disputeService->addMessage($dispute, $validated);

return response()->json($message, 201);
}
}
15 changes: 15 additions & 0 deletions app/Models/Invoice.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,21 @@ protected static function boot()
});
}

public function disputes()
{
return $this->hasMany(InvoiceDispute::class);
}

public function activeDispute()
{
return $this->disputes()->whereIn('status', ['open', 'under_review'])->latest()->first();
}

public function isDisputed()
{
return $this->activeDispute() !== null;
}

protected $fillable = [
'customer_id',
'invoice_number',
Expand Down
50 changes: 50 additions & 0 deletions app/Models/InvoiceDispute.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@


<?php

namespace App\Models;

use App\Traits\HasTeam;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;

class InvoiceDispute extends Model
{
use HasFactory;
use HasTeam;

protected $fillable = [
'invoice_id',
'customer_id',
'status',
'reason',
'description',
'resolution_notes',
'resolved_at',
'resolved_by'
];

protected $casts = [
'resolved_at' => 'datetime'
];

public function invoice()
{
return $this->belongsTo(Invoice::class);
}

public function customer()
{
return $this->belongsTo(Customer::class);
}

public function resolver()
{
return $this->belongsTo(User::class, 'resolved_by');
}

public function messages()
{
return $this->hasMany(DisputeMessage::class);
}
}
90 changes: 90 additions & 0 deletions app/Services/DisputeService.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@


<?php

namespace App\Services;

use App\Models\Invoice;
use App\Models\InvoiceDispute;
use App\Models\DisputeMessage;
use App\Models\User;
use App\Mail\DisputeCreated;
use App\Mail\DisputeStatusUpdated;
use App\Mail\DisputeMessageReceived;
use Illuminate\Support\Facades\Mail;

class DisputeService
{
public function createDispute(Invoice $invoice, array $data)
{
if ($invoice->isDisputed()) {
throw new \Exception('Invoice already has an active dispute');
}

$dispute = InvoiceDispute::create([
'invoice_id' => $invoice->id,
'customer_id' => $invoice->customer_id,
'status' => 'open',
'reason' => $data['reason'],
'description' => $data['description']
]);

$invoice->update(['status' => 'disputed']);
$this->sendDisputeNotifications($dispute, 'created');

return $dispute;
}

public function updateDisputeStatus(InvoiceDispute $dispute, string $status, string $notes = null)
{
$dispute->update([
'status' => $status,
'resolution_notes' => $notes,
'resolved_at' => in_array($status, ['resolved', 'rejected']) ? now() : null,
'resolved_by' => in_array($status, ['resolved', 'rejected']) ? auth()->id() : null
]);

if ($status === 'resolved') {
$dispute->invoice->update(['status' => 'pending']);
}

$this->sendDisputeNotifications($dispute, 'status_updated');

return $dispute;
}

public function addMessage(InvoiceDispute $dispute, array $data)
{
$message = $dispute->messages()->create([
'user_id' => auth()->id(),
'message' => $data['message'],
'attachments' => $data['attachments'] ?? null
]);

$this->sendDisputeNotifications($dispute, 'new_message');

return $message;
}

protected function sendDisputeNotifications(InvoiceDispute $dispute, string $type)
{
$customer = $dispute->customer;
$adminUsers = User::where('team_id', $customer->team_id)
->whereHas('roles', function($q) {
$q->where('name', 'admin');
})->get();

switch($type) {
case 'created':
Mail::to($adminUsers)->send(new DisputeCreated($dispute));
break;
case 'status_updated':
Mail::to($customer->email)->send(new DisputeStatusUpdated($dispute));
break;
case 'new_message':
$recipient = auth()->id() === $customer->id ? $adminUsers : $customer;
Mail::to($recipient)->send(new DisputeMessageReceived($dispute));
break;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@


<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
public function up()
{
Schema::create('invoice_disputes', function (Blueprint $table) {
$table->id();
$table->foreignId('invoice_id')->constrained()->onDelete('cascade');
$table->foreignId('customer_id')->constrained();
$table->string('status')->default('open');
$table->string('reason');
$table->text('description');
$table->text('resolution_notes')->nullable();
$table->timestamp('resolved_at')->nullable();
$table->foreignId('resolved_by')->nullable()->constrained('users');
$table->timestamps();
});
}

public function down()
{
Schema::dropIfExists('invoice_disputes');
}
};

0 comments on commit 8d32534

Please sign in to comment.