PluginProbe
Easy Invoice – Invoice Generator, PDF Quotes & Payments / 2.4.1
Easy Invoice – Invoice Generator, PDF Quotes & Payments v2.4.1
2.4.0 2.4.1 2.3.8 2.3.7 2.3.6 2.3.5 2.3.4 2.3.3 2.3.2 2.3.1 2.2.0 2.1.21 2.1.20 2.1.19 2.1.18 2.1.0 2.1.1 2.1.10 2.1.11 2.1.12 2.1.13 2.1.14 2.1.15 2.1.16 2.1.2 All 57 releases
easy-invoice / includes / Controllers / CreditNoteController.php

CreditNoteController.php in Easy Invoice – Invoice Generator, PDF Quotes & Payments 2.4.1, at includes/Controllers/CreditNoteController.php

258 lines 9.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Admin surface for credit notes.
4 *
5 * @package Easy_Invoice
6 * @subpackage Controllers
7 */
8
9 namespace EasyInvoice\Controllers;
10
11 use EasyInvoice\Constants\PostTypes;
12 use EasyInvoice\Services\CreditNote;
13 use EasyInvoice\Services\PdfRenderer;
14
15 if ( ! defined( 'ABSPATH' ) ) {
16 exit;
17 }
18
19 /**
20 * Lets a merchant issue and download credit notes.
21 *
22 * Reached from the invoice list rather than a menu of its own: a credit note
23 * only ever exists against an invoice, so the place to ask for one is the row
24 * of the invoice being corrected. There is no "new credit note" button for the
25 * same reason — one without a source invoice is not a credit note, it is a
26 * mystery.
27 */
28 class CreditNoteController {
29
30 /** Admin page slug. */
31 const PAGE_SLUG = 'easy-invoice-credit-note';
32
33 /** admin-post action that issues a credit note. */
34 const ACTION_CREATE = 'easy_invoice_create_credit_note';
35
36 /** admin-post action that serves a credit note PDF. */
37 const ACTION_DOWNLOAD = 'easy_invoice_download_credit_note';
38
39 /**
40 * Wire it up.
41 *
42 * @return void
43 */
44 public static function init(): void {
45 add_action( 'admin_menu', [ __CLASS__, 'registerPage' ], 99 );
46 add_action( 'easy_invoice_admin_main_content', [ __CLASS__, 'maybeRenderPage' ], 11 );
47
48 add_action( 'admin_post_' . self::ACTION_CREATE, [ __CLASS__, 'handleCreate' ] );
49 add_action( 'admin_post_' . self::ACTION_DOWNLOAD, [ __CLASS__, 'handleDownload' ] );
50
51 add_filter( 'easy_invoice_invoice_row_actions', [ __CLASS__, 'addRowAction' ], 10, 2 );
52 }
53
54 /**
55 * Register the hidden page the shell renders into.
56 *
57 * @return void
58 */
59 public static function registerPage(): void {
60 add_submenu_page(
61 'easy-invoice-hidden',
62 __( 'Credit Note', 'easy-invoice' ),
63 __( 'Credit Note', 'easy-invoice' ),
64 (string) apply_filters( 'easy_invoice_menu_capability', 'manage_options', self::PAGE_SLUG ),
65 self::PAGE_SLUG,
66 [ __CLASS__, 'renderShell' ]
67 );
68 }
69
70 /**
71 * Render the plugin's admin chrome.
72 *
73 * @return void
74 */
75 public static function renderShell(): void {
76 include EASY_INVOICE_PLUGIN_DIR . 'templates/main-template.php';
77 }
78
79 /**
80 * Render our page when the shell asks for it.
81 *
82 * @param string $page Page slug being rendered.
83 * @return void
84 */
85 public static function maybeRenderPage( $page ): void {
86 if ( self::PAGE_SLUG !== $page || ! easy_invoice_user_can( 'ei_view_invoices' ) ) {
87 return;
88 }
89
90 $invoice_id = isset( $_GET['invoice_id'] ) ? absint( $_GET['invoice_id'] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only view.
91 $post = get_post( $invoice_id );
92
93 if ( ! $post instanceof \WP_Post || PostTypes::EASY_INVOICE_POST_TYPE !== $post->post_type ) {
94 echo '<div class="p-8"><p>' . esc_html__( 'That invoice does not exist.', 'easy-invoice' ) . '</p></div>';
95 return;
96 }
97
98 $invoice = new \EasyInvoice\Models\Invoice( $post );
99 $remaining = CreditNote::remainingCreditable( $invoice_id );
100 $credited = CreditNote::creditedTotal( $invoice_id );
101 $notes = CreditNote::forInvoice( $invoice_id );
102 $notice = isset( $_GET['cn_notice'] ) ? sanitize_text_field( wp_unslash( $_GET['cn_notice'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
103 $can_issue = easy_invoice_user_can( 'ei_create_invoice' );
104
105 include EASY_INVOICE_PLUGIN_DIR . 'templates/admin/credit-note-page.php';
106 }
107
108 /**
109 * Add a credit-note link to each invoice row.
110 *
111 * @param array $actions Actions contributed so far.
112 * @param object $invoice Invoice model.
113 * @return array
114 */
115 public static function addRowAction( $actions, $invoice ): array {
116 $actions = is_array( $actions ) ? $actions : [];
117
118 if ( ! easy_invoice_user_can( 'ei_view_invoices' ) ) {
119 return $actions;
120 }
121
122 $id = is_callable( [ $invoice, 'getId' ] ) ? (int) $invoice->getId() : 0;
123 if ( $id <= 0 || ! \EasyInvoice\Services\InvoiceRetention::isIssued( $id ) ) {
124 // Drafts are edited, not credited.
125 return $actions;
126 }
127
128 $credited = CreditNote::creditedTotal( $id );
129
130 $actions['credit_note'] = sprintf(
131 '<a href="%s">%s</a>',
132 esc_url( add_query_arg(
133 [
134 'page' => self::PAGE_SLUG,
135 'invoice_id' => $id,
136 ],
137 admin_url( 'admin.php' )
138 ) ),
139 $credited > 0
140 ? esc_html__( 'Credited', 'easy-invoice' )
141 : esc_html__( 'Credit note', 'easy-invoice' )
142 );
143
144 return $actions;
145 }
146
147 /**
148 * Issue a credit note.
149 *
150 * @return void
151 */
152 public static function handleCreate(): void {
153 check_admin_referer( self::ACTION_CREATE );
154
155 // Issuing a credit note reduces what a customer owes. It is the same
156 // class of act as issuing an invoice, so it takes the same capability --
157 // a Viewer can read every credit note and issue none.
158 if ( ! easy_invoice_user_can( 'ei_create_invoice' ) ) {
159 wp_die( esc_html__( 'You do not have permission to issue credit notes.', 'easy-invoice' ), '', [ 'response' => 403 ] );
160 }
161
162 $invoice_id = isset( $_POST['invoice_id'] ) ? absint( $_POST['invoice_id'] ) : 0;
163 $reason = isset( $_POST['reason'] ) ? sanitize_textarea_field( wp_unslash( $_POST['reason'] ) ) : '';
164
165 // An empty amount means "all of it", which is the common case: this
166 // invoice was wrong, cancel it out.
167 $amount = null;
168 if ( isset( $_POST['amount'] ) && '' !== trim( (string) wp_unslash( $_POST['amount'] ) ) ) {
169 $amount = (float) str_replace( ',', '.', (string) wp_unslash( $_POST['amount'] ) );
170 }
171
172 $result = CreditNote::create( $invoice_id, [
173 'amount' => $amount,
174 'reason' => $reason,
175 ] );
176
177 $back = add_query_arg(
178 [
179 'page' => self::PAGE_SLUG,
180 'invoice_id' => $invoice_id,
181 ],
182 admin_url( 'admin.php' )
183 );
184
185 if ( is_wp_error( $result ) ) {
186 wp_safe_redirect( add_query_arg( 'cn_notice', rawurlencode( $result->get_error_message() ), $back ) );
187 exit;
188 }
189
190 wp_safe_redirect( add_query_arg( 'cn_notice', rawurlencode( __( 'Credit note issued.', 'easy-invoice' ) ), $back ) );
191 exit;
192 }
193
194 /**
195 * Serve a credit note as a PDF.
196 *
197 * @return void
198 */
199 public static function handleDownload(): void {
200 $credit_id = isset( $_GET['credit_id'] ) ? absint( $_GET['credit_id'] ) : 0;
201 $nonce = isset( $_GET['_wpnonce'] ) ? sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ) : '';
202
203 if ( ! wp_verify_nonce( $nonce, self::ACTION_DOWNLOAD . '_' . $credit_id ) ) {
204 wp_die( esc_html__( 'Security check failed.', 'easy-invoice' ), '', [ 'response' => 403 ] );
205 }
206
207 if ( ! easy_invoice_user_can( 'ei_view_invoices' ) ) {
208 wp_die( esc_html__( 'You do not have permission to download this document.', 'easy-invoice' ), '', [ 'response' => 403 ] );
209 }
210
211 $post = get_post( $credit_id );
212 if ( ! $post instanceof \WP_Post || PostTypes::EASY_INVOICE_CREDIT_NOTE_POST_TYPE !== $post->post_type ) {
213 wp_die( esc_html__( 'That credit note does not exist.', 'easy-invoice' ), '', [ 'response' => 404 ] );
214 }
215
216 $document = new \EasyInvoice\Models\Invoice( $post );
217 $pdf = PdfRenderer::renderInvoice( $document );
218
219 if ( is_wp_error( $pdf ) ) {
220 wp_die( esc_html( $pdf->get_error_message() ), '', [ 'response' => 500, 'back_link' => true ] );
221 }
222
223 $number = sanitize_file_name( (string) $document->getNumber() ?: 'credit-note' );
224
225 if ( ob_get_length() ) {
226 @ob_end_clean(); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
227 }
228
229 nocache_headers();
230 header( 'Content-Type: application/pdf' );
231 header( 'Content-Disposition: attachment; filename="' . $number . '.pdf"' );
232 header( 'Content-Length: ' . strlen( $pdf ) );
233 header( 'X-Content-Type-Options: nosniff' );
234
235 echo $pdf; // phpcs:ignore WordPress.Security.EscapeOutput -- binary document.
236 exit;
237 }
238
239 /**
240 * URL that downloads a credit note.
241 *
242 * @param int $credit_id Credit note ID.
243 * @return string
244 */
245 public static function downloadUrl( int $credit_id ): string {
246 return wp_nonce_url(
247 add_query_arg(
248 [
249 'action' => self::ACTION_DOWNLOAD,
250 'credit_id' => $credit_id,
251 ],
252 admin_url( 'admin-post.php' )
253 ),
254 self::ACTION_DOWNLOAD . '_' . $credit_id
255 );
256 }
257 }
258