1414
1515class CreditNoteController extends Controller
1616{
17- public function index (): Response
17+ public function index (Request $ request ): Response
1818 {
1919 $ this ->authorize ('viewAny ' , CreditNote::class);
2020
21- $ creditNotes = CreditNote::with (['contact ' ])
22- ->orderByDesc ('issue_date ' )
23- ->paginate (25 );
21+ $ query = CreditNote::with (['contact ' ])
22+ ->orderByDesc ('issue_date ' );
23+
24+ if ($ request ->filled ('status ' )) {
25+ $ query ->where ('status ' , $ request ->status );
26+ }
27+
28+ $ creditNotes = $ query ->paginate (20 );
2429
2530 return Inertia::render ('Finance/CreditNotes/Index ' , compact ('creditNotes ' ));
2631 }
2732
28- public function create (Request $ request ): Response
33+ public function create (): Response
2934 {
3035 $ this ->authorize ('create ' , CreditNote::class);
3136
@@ -34,69 +39,120 @@ public function create(Request $request): Response
3439 ->orderByDesc ('issue_date ' )->get (['id ' , 'number ' ]);
3540 $ bills = Bill::where ('status ' , 'received ' )
3641 ->orderByDesc ('issue_date ' )->get (['id ' , 'number ' ]);
37- $ type = $ request ->get ('type ' , 'sale ' );
3842
39- return Inertia::render ('Finance/CreditNotes/Create ' , compact ('contacts ' , 'invoices ' , 'bills ' , ' type ' ));
43+ return Inertia::render ('Finance/CreditNotes/Create ' , compact ('contacts ' , 'invoices ' , 'bills ' ));
4044 }
4145
4246 public function store (Request $ request ): RedirectResponse
4347 {
4448 $ this ->authorize ('create ' , CreditNote::class);
4549
46- $ data = $ request ->validate ([
47- 'reference ' => 'required|string|max:100 ' ,
48- 'contact_id ' => 'nullable|exists:contacts,id ' ,
49- 'original_invoice_id ' => 'nullable|exists:invoices,id ' ,
50- 'original_bill_id ' => 'nullable|exists:bills,id ' ,
51- 'type ' => 'required|in:sale,purchase ' ,
52- 'issue_date ' => 'required|date ' ,
53- 'currency_code ' => 'required|string|size:3 ' ,
54- 'exchange_rate ' => 'required|numeric|min:0.000001 ' ,
55- 'notes ' => 'nullable|string ' ,
56- 'items ' => 'required|array|min:1 ' ,
57- 'items.*.description ' => 'required|string ' ,
58- 'items.*.quantity ' => 'required|numeric|min:0.01 ' ,
59- 'items.*.unit_price ' => 'required|numeric|min:0 ' ,
60- 'items.*.tax_rate ' => 'required|numeric|min:0|max:100 ' ,
61- ]);
62-
63- $ cn = CreditNote::create ([
64- 'tenant_id ' => auth ()->user ()->tenant_id ,
65- 'reference ' => $ data ['reference ' ],
66- 'contact_id ' => $ data ['contact_id ' ] ?? null ,
67- 'original_invoice_id ' => $ data ['original_invoice_id ' ] ?? null ,
68- 'original_bill_id ' => $ data ['original_bill_id ' ] ?? null ,
69- 'type ' => $ data ['type ' ],
70- 'status ' => 'draft ' ,
71- 'issue_date ' => $ data ['issue_date ' ],
72- 'currency_code ' => $ data ['currency_code ' ],
73- 'exchange_rate ' => $ data ['exchange_rate ' ],
74- 'notes ' => $ data ['notes ' ] ?? null ,
75- 'subtotal ' => 0 ,
76- 'tax_total ' => 0 ,
77- 'total ' => 0 ,
78- 'amount_applied ' => 0 ,
79- ]);
80-
81- foreach ($ data ['items ' ] as $ item ) {
82- $ cn ->items ()->create ([
83- 'description ' => $ item ['description ' ],
84- 'quantity ' => $ item ['quantity ' ],
85- 'unit_price ' => $ item ['unit_price ' ],
86- 'tax_rate ' => $ item ['tax_rate ' ],
87- 'line_total ' => round ($ item ['quantity ' ] * $ item ['unit_price ' ], 2 ),
50+ // Detect whether this is a Phase 103 request (no legacy required fields)
51+ $ isPhase103 = ! $ request ->has ('reference ' );
52+
53+ if ($ isPhase103 ) {
54+ $ data = $ request ->validate ([
55+ 'issue_date ' => 'required|date ' ,
56+ 'currency ' => 'nullable|string|max:3 ' ,
57+ 'reason ' => 'nullable|string ' ,
58+ 'notes ' => 'nullable|string ' ,
59+ 'original_invoice_id ' => 'nullable|integer ' ,
60+ 'items ' => 'required|array|min:1 ' ,
61+ 'items.*.description ' => 'required|string|max:255 ' ,
62+ 'items.*.quantity ' => 'required|numeric|min:0.01 ' ,
63+ 'items.*.unit_price ' => 'required|numeric|min:0 ' ,
64+ ]);
65+
66+ $ creditNoteNumber = CreditNote::generateCreditNoteNumber ();
67+
68+ $ cn = CreditNote::create ([
69+ 'tenant_id ' => auth ()->user ()->tenant_id ,
70+ 'credit_note_number ' => $ creditNoteNumber ,
71+ 'reference ' => $ creditNoteNumber , // satisfy NOT NULL if column exists
72+ 'type ' => 'sale ' , // satisfy enum NOT NULL
73+ 'original_invoice_id ' => $ data ['original_invoice_id ' ] ?? null ,
74+ 'status ' => 'draft ' ,
75+ 'issue_date ' => $ data ['issue_date ' ],
76+ 'currency_code ' => $ data ['currency ' ] ?? 'USD ' ,
77+ 'currency ' => $ data ['currency ' ] ?? 'USD ' ,
78+ 'exchange_rate ' => 1 ,
79+ 'subtotal ' => 0 ,
80+ 'tax ' => 0 ,
81+ 'tax_total ' => 0 ,
82+ 'total ' => 0 ,
83+ 'amount_applied ' => 0 ,
84+ 'reason ' => $ data ['reason ' ] ?? null ,
85+ 'notes ' => $ data ['notes ' ] ?? null ,
86+ 'created_by ' => auth ()->id (),
87+ ]);
88+
89+ foreach ($ data ['items ' ] as $ item ) {
90+ $ cn ->items ()->create ([
91+ 'description ' => $ item ['description ' ],
92+ 'quantity ' => $ item ['quantity ' ],
93+ 'unit_price ' => $ item ['unit_price ' ],
94+ 'tax_rate ' => 0 ,
95+ ]);
96+ }
97+
98+ $ cn ->recalculateTotals ();
99+ } else {
100+ // Legacy path (original implementation)
101+ $ data = $ request ->validate ([
102+ 'reference ' => 'required|string|max:100 ' ,
103+ 'contact_id ' => 'nullable|exists:contacts,id ' ,
104+ 'original_invoice_id ' => 'nullable|exists:invoices,id ' ,
105+ 'original_bill_id ' => 'nullable|exists:bills,id ' ,
106+ 'type ' => 'required|in:sale,purchase ' ,
107+ 'issue_date ' => 'required|date ' ,
108+ 'currency_code ' => 'required|string|size:3 ' ,
109+ 'exchange_rate ' => 'required|numeric|min:0.000001 ' ,
110+ 'notes ' => 'nullable|string ' ,
111+ 'items ' => 'required|array|min:1 ' ,
112+ 'items.*.description ' => 'required|string ' ,
113+ 'items.*.quantity ' => 'required|numeric|min:0.01 ' ,
114+ 'items.*.unit_price ' => 'required|numeric|min:0 ' ,
115+ 'items.*.tax_rate ' => 'required|numeric|min:0|max:100 ' ,
116+ ]);
117+
118+ $ cn = CreditNote::create ([
119+ 'tenant_id ' => auth ()->user ()->tenant_id ,
120+ 'reference ' => $ data ['reference ' ],
121+ 'contact_id ' => $ data ['contact_id ' ] ?? null ,
122+ 'original_invoice_id ' => $ data ['original_invoice_id ' ] ?? null ,
123+ 'original_bill_id ' => $ data ['original_bill_id ' ] ?? null ,
124+ 'type ' => $ data ['type ' ],
125+ 'status ' => 'draft ' ,
126+ 'issue_date ' => $ data ['issue_date ' ],
127+ 'currency_code ' => $ data ['currency_code ' ],
128+ 'exchange_rate ' => $ data ['exchange_rate ' ],
129+ 'notes ' => $ data ['notes ' ] ?? null ,
130+ 'subtotal ' => 0 ,
131+ 'tax_total ' => 0 ,
132+ 'total ' => 0 ,
133+ 'amount_applied ' => 0 ,
88134 ]);
89- }
90135
91- // Recalculate totals explicitly after items are created
92- $ cn ->refresh ()->load ('items ' );
93- $ subtotal = $ cn ->items ->sum ('line_total ' );
94- $ tax_total = $ cn ->items ->sum (fn ($ i ) => $ i ->line_total * $ i ->tax_rate / 100 );
95- $ cn ->update ([
96- 'subtotal ' => $ subtotal ,
97- 'tax_total ' => $ tax_total ,
98- 'total ' => $ subtotal + $ tax_total ,
99- ]);
136+ foreach ($ data ['items ' ] as $ item ) {
137+ $ cn ->items ()->create ([
138+ 'description ' => $ item ['description ' ],
139+ 'quantity ' => $ item ['quantity ' ],
140+ 'unit_price ' => $ item ['unit_price ' ],
141+ 'tax_rate ' => $ item ['tax_rate ' ],
142+ 'line_total ' => round ($ item ['quantity ' ] * $ item ['unit_price ' ], 2 ),
143+ ]);
144+ }
145+
146+ // Recalculate totals explicitly after items are created
147+ $ cn ->refresh ()->load ('items ' );
148+ $ subtotal = $ cn ->items ->sum ('line_total ' );
149+ $ tax_total = $ cn ->items ->sum (fn ($ i ) => $ i ->line_total * $ i ->tax_rate / 100 );
150+ $ cn ->update ([
151+ 'subtotal ' => $ subtotal ,
152+ 'tax_total ' => $ tax_total ,
153+ 'total ' => $ subtotal + $ tax_total ,
154+ ]);
155+ }
100156
101157 return redirect ()->route ('finance.credit-notes.show ' , $ cn )
102158 ->with ('success ' , 'Credit note created. ' );
@@ -116,17 +172,27 @@ public function issue(CreditNote $creditNote): RedirectResponse
116172 $ this ->authorize ('update ' , $ creditNote );
117173
118174 abort_unless ($ creditNote ->status === 'draft ' , 422 , 'Only draft credit notes can be issued. ' );
119- $ creditNote ->update ([ ' status ' => ' issued ' ] );
175+ $ creditNote ->issue ( );
120176
121177 return back ()->with ('success ' , 'Credit note issued. ' );
122178 }
123179
180+ public function apply (CreditNote $ creditNote ): RedirectResponse
181+ {
182+ $ this ->authorize ('update ' , $ creditNote );
183+
184+ abort_unless ($ creditNote ->status === 'issued ' , 422 , 'Only issued credit notes can be applied. ' );
185+ $ creditNote ->apply ();
186+
187+ return back ()->with ('success ' , 'Credit note applied. ' );
188+ }
189+
124190 public function void (CreditNote $ creditNote ): RedirectResponse
125191 {
126192 $ this ->authorize ('update ' , $ creditNote );
127193
128194 abort_unless (in_array ($ creditNote ->status , ['draft ' , 'issued ' ]), 422 , 'Cannot void applied credit notes. ' );
129- $ creditNote ->update ([ ' status ' => ' void ' ] );
195+ $ creditNote ->void ( );
130196
131197 return back ()->with ('success ' , 'Credit note voided. ' );
132198 }
0 commit comments