# easy-invoice/2.4.1/includes/Controllers/CreditNoteController.php

Easy Invoice – Invoice Generator, PDF Quotes &amp; Payments, version 2.4.1. 258 lines.

- Page: https://pluginprobe.com/plugins/easy-invoice/2.4.1/code/includes/Controllers/CreditNoteController.php
- Raw: https://pluginprobe.com/plugins/easy-invoice/2.4.1/raw/includes/Controllers/CreditNoteController.php
- Modified: 2026-09-15T12:31:20+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/easy-invoice/2.4.1/code/includes/Controllers/CreditNoteController.php#L10-L20`.

```php
<?php
/**
 * Admin surface for credit notes.
 *
 * @package Easy_Invoice
 * @subpackage Controllers
 */

namespace EasyInvoice\Controllers;

use EasyInvoice\Constants\PostTypes;
use EasyInvoice\Services\CreditNote;
use EasyInvoice\Services\PdfRenderer;

if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

/**
 * Lets a merchant issue and download credit notes.
 *
 * Reached from the invoice list rather than a menu of its own: a credit note
 * only ever exists against an invoice, so the place to ask for one is the row
 * of the invoice being corrected. There is no "new credit note" button for the
 * same reason — one without a source invoice is not a credit note, it is a
 * mystery.
 */
class CreditNoteController {

    /** Admin page slug. */
    const PAGE_SLUG = 'easy-invoice-credit-note';

    /** admin-post action that issues a credit note. */
    const ACTION_CREATE = 'easy_invoice_create_credit_note';

    /** admin-post action that serves a credit note PDF. */
    const ACTION_DOWNLOAD = 'easy_invoice_download_credit_note';

    /**
     * Wire it up.
     *
     * @return void
     */
    public static function init(): void {
        add_action( 'admin_menu', [ __CLASS__, 'registerPage' ], 99 );
        add_action( 'easy_invoice_admin_main_content', [ __CLASS__, 'maybeRenderPage' ], 11 );

        add_action( 'admin_post_' . self::ACTION_CREATE, [ __CLASS__, 'handleCreate' ] );
        add_action( 'admin_post_' . self::ACTION_DOWNLOAD, [ __CLASS__, 'handleDownload' ] );

        add_filter( 'easy_invoice_invoice_row_actions', [ __CLASS__, 'addRowAction' ], 10, 2 );
    }

    /**
     * Register the hidden page the shell renders into.
     *
     * @return void
     */
    public static function registerPage(): void {
        add_submenu_page(
            'easy-invoice-hidden',
            __( 'Credit Note', 'easy-invoice' ),
            __( 'Credit Note', 'easy-invoice' ),
            (string) apply_filters( 'easy_invoice_menu_capability', 'manage_options', self::PAGE_SLUG ),
            self::PAGE_SLUG,
            [ __CLASS__, 'renderShell' ]
        );
    }

    /**
     * Render the plugin's admin chrome.
     *
     * @return void
     */
    public static function renderShell(): void {
        include EASY_INVOICE_PLUGIN_DIR . 'templates/main-template.php';
    }

    /**
     * Render our page when the shell asks for it.
     *
     * @param string $page Page slug being rendered.
     * @return void
     */
    public static function maybeRenderPage( $page ): void {
        if ( self::PAGE_SLUG !== $page || ! easy_invoice_user_can( 'ei_view_invoices' ) ) {
            return;
        }

        $invoice_id = isset( $_GET['invoice_id'] ) ? absint( $_GET['invoice_id'] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only view.
        $post       = get_post( $invoice_id );

        if ( ! $post instanceof \WP_Post || PostTypes::EASY_INVOICE_POST_TYPE !== $post->post_type ) {
            echo '<div class="p-8"><p>' . esc_html__( 'That invoice does not exist.', 'easy-invoice' ) . '</p></div>';
            return;
        }

        $invoice   = new \EasyInvoice\Models\Invoice( $post );
        $remaining = CreditNote::remainingCreditable( $invoice_id );
        $credited  = CreditNote::creditedTotal( $invoice_id );
        $notes     = CreditNote::forInvoice( $invoice_id );
        $notice    = isset( $_GET['cn_notice'] ) ? sanitize_text_field( wp_unslash( $_GET['cn_notice'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
        $can_issue = easy_invoice_user_can( 'ei_create_invoice' );

        include EASY_INVOICE_PLUGIN_DIR . 'templates/admin/credit-note-page.php';
    }

    /**
     * Add a credit-note link to each invoice row.
     *
     * @param array  $actions Actions contributed so far.
     * @param object $invoice Invoice model.
     * @return array
     */
    public static function addRowAction( $actions, $invoice ): array {
        $actions = is_array( $actions ) ? $actions : [];

        if ( ! easy_invoice_user_can( 'ei_view_invoices' ) ) {
            return $actions;
        }

        $id = is_callable( [ $invoice, 'getId' ] ) ? (int) $invoice->getId() : 0;
        if ( $id <= 0 || ! \EasyInvoice\Services\InvoiceRetention::isIssued( $id ) ) {
            // Drafts are edited, not credited.
            return $actions;
        }

        $credited = CreditNote::creditedTotal( $id );

        $actions['credit_note'] = sprintf(
            '<a href="%s">%s</a>',
            esc_url( add_query_arg(
                [
                    'page'       => self::PAGE_SLUG,
                    'invoice_id' => $id,
                ],
                admin_url( 'admin.php' )
            ) ),
            $credited > 0
                ? esc_html__( 'Credited', 'easy-invoice' )
                : esc_html__( 'Credit note', 'easy-invoice' )
        );

        return $actions;
    }

    /**
     * Issue a credit note.
     *
     * @return void
     */
    public static function handleCreate(): void {
        check_admin_referer( self::ACTION_CREATE );

        // Issuing a credit note reduces what a customer owes. It is the same
        // class of act as issuing an invoice, so it takes the same capability --
        // a Viewer can read every credit note and issue none.
        if ( ! easy_invoice_user_can( 'ei_create_invoice' ) ) {
            wp_die( esc_html__( 'You do not have permission to issue credit notes.', 'easy-invoice' ), '', [ 'response' => 403 ] );
        }

        $invoice_id = isset( $_POST['invoice_id'] ) ? absint( $_POST['invoice_id'] ) : 0;
        $reason     = isset( $_POST['reason'] ) ? sanitize_textarea_field( wp_unslash( $_POST['reason'] ) ) : '';

        // An empty amount means "all of it", which is the common case: this
        // invoice was wrong, cancel it out.
        $amount = null;
        if ( isset( $_POST['amount'] ) && '' !== trim( (string) wp_unslash( $_POST['amount'] ) ) ) {
            $amount = (float) str_replace( ',', '.', (string) wp_unslash( $_POST['amount'] ) );
        }

        $result = CreditNote::create( $invoice_id, [
            'amount' => $amount,
            'reason' => $reason,
        ] );

        $back = add_query_arg(
            [
                'page'       => self::PAGE_SLUG,
                'invoice_id' => $invoice_id,
            ],
            admin_url( 'admin.php' )
        );

        if ( is_wp_error( $result ) ) {
            wp_safe_redirect( add_query_arg( 'cn_notice', rawurlencode( $result->get_error_message() ), $back ) );
            exit;
        }

        wp_safe_redirect( add_query_arg( 'cn_notice', rawurlencode( __( 'Credit note issued.', 'easy-invoice' ) ), $back ) );
        exit;
    }

    /**
     * Serve a credit note as a PDF.
     *
     * @return void
     */
    public static function handleDownload(): void {
        $credit_id = isset( $_GET['credit_id'] ) ? absint( $_GET['credit_id'] ) : 0;
        $nonce     = isset( $_GET['_wpnonce'] ) ? sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ) : '';

        if ( ! wp_verify_nonce( $nonce, self::ACTION_DOWNLOAD . '_' . $credit_id ) ) {
            wp_die( esc_html__( 'Security check failed.', 'easy-invoice' ), '', [ 'response' => 403 ] );
        }

        if ( ! easy_invoice_user_can( 'ei_view_invoices' ) ) {
            wp_die( esc_html__( 'You do not have permission to download this document.', 'easy-invoice' ), '', [ 'response' => 403 ] );
        }

        $post = get_post( $credit_id );
        if ( ! $post instanceof \WP_Post || PostTypes::EASY_INVOICE_CREDIT_NOTE_POST_TYPE !== $post->post_type ) {
            wp_die( esc_html__( 'That credit note does not exist.', 'easy-invoice' ), '', [ 'response' => 404 ] );
        }

        $document = new \EasyInvoice\Models\Invoice( $post );
        $pdf      = PdfRenderer::renderInvoice( $document );

        if ( is_wp_error( $pdf ) ) {
            wp_die( esc_html( $pdf->get_error_message() ), '', [ 'response' => 500, 'back_link' => true ] );
        }

        $number = sanitize_file_name( (string) $document->getNumber() ?: 'credit-note' );

        if ( ob_get_length() ) {
            @ob_end_clean(); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
        }

        nocache_headers();
        header( 'Content-Type: application/pdf' );
        header( 'Content-Disposition: attachment; filename="' . $number . '.pdf"' );
        header( 'Content-Length: ' . strlen( $pdf ) );
        header( 'X-Content-Type-Options: nosniff' );

        echo $pdf; // phpcs:ignore WordPress.Security.EscapeOutput -- binary document.
        exit;
    }

    /**
     * URL that downloads a credit note.
     *
     * @param int $credit_id Credit note ID.
     * @return string
     */
    public static function downloadUrl( int $credit_id ): string {
        return wp_nonce_url(
            add_query_arg(
                [
                    'action'    => self::ACTION_DOWNLOAD,
                    'credit_id' => $credit_id,
                ],
                admin_url( 'admin-post.php' )
            ),
            self::ACTION_DOWNLOAD . '_' . $credit_id
        );
    }
}

```
