/home/edulekha/crm.edulekha.com/modules/einvoice/controllers/Einvoice.php
<?php
use Perfexcrm\EInvoice\BulkExporter;
use Perfexcrm\EInvoice\BulkExporterConfig;
use Perfexcrm\EInvoice\EinvoiceHandler;
use Perfexcrm\EInvoice\OutputWriter;
defined('BASEPATH') or exit('No direct script access allowed');
/**
* @property-read Credit_notes_model|null $credit_notes_model
* @property-read Invoices_model|null $invoices_model
* @property-read Templates_model $templates_model
*/
class Einvoice extends AdminController
{
public function template($id = ''): void
{
$this->app_scripts->add('codemirror-js', module_dir_url('einvoice', 'assets/builds/codemirror.js'));
$this->app_css->add('codemirror-css', module_dir_url('einvoice', 'assets/builds/codemirror.css'));
$this->app_scripts->add('einvoice-js', module_dir_url('einvoice', 'assets/builds/template.js'));
$data = ['template' => null];
if ($id !== '') {
$this->load->model('templates_model');
$template = $this->templates_model->find($id);
if ($template === null || $template->type !== 'einvoice') {
show_404();
}
$data['template'] = $template;
}
$data['title'] = _l('settings_einvoice_templates');
$this->load->view('einvoice/template', $data);
}
public function validate_template_ajax(): void
{
if (! $this->input->is_ajax_request() || ! is_admin()) {
ajax_access_denied();
}
$content = $this->input->post('content', false) ?? '';
$contentType = $this->input->post('content_type') ?? '';
$validation = $this->validateTemplate($contentType, $content);
if ($validation['valid']) {
echo json_encode([
'success' => true,
'message' => _l('template_validation_success'),
]);
} else {
echo json_encode([
'success' => false,
'message' => $validation['error'],
]);
}
}
public function validate_and_save($id = null): void
{
if (! is_admin()) {
access_denied();
}
// Only handle AJAX requests
if (! $this->input->is_ajax_request()) {
show_404();
return;
}
$content = $this->input->post('content', false) ?? '';
$contentType = $this->input->post('content_type') ?? '';
$validation = $this->validateTemplate($contentType, $content);
if (! $validation['valid']) {
echo json_encode([
'success' => false,
'message' => $validation['error'],
]);
return;
}
$data['name'] = $this->input->post('name');
$data['addedfrom'] = get_staff_user_id();
$data['type'] = 'einvoice';
$data['content'] = $content;
$data['content_type'] = $contentType;
$this->load->model('templates_model');
if (is_numeric($id)) {
$template = $this->templates_model->find($id);
if (! $template) {
echo json_encode([
'success' => false,
'message' => _l('access_denied'),
]);
return;
}
$success = $this->templates_model->update($id, $data);
$message = _l('template_updated');
} else {
$success = $this->templates_model->create($data);
$message = _l('template_added');
$id = $success; // For new templates, success returns the ID
}
if ($success) {
echo json_encode([
'success' => true,
'message' => $message,
'redirect' => admin_url('settings?group=einvoice'),
]);
} else {
echo json_encode([
'success' => false,
'message' => _l('something_went_wrong'),
]);
}
}
private function validateTemplate(string $type, string $content)
{
if (empty(trim($content))) {
return [
'valid' => false,
'error' => _l('template_content_required'),
];
}
try {
$content = (new EinvoiceHandler())->renderTemplate($content, [], $type);
} catch (Exception $e) {
return [
'valid' => false,
'error' => _l('einvoice_template_invalid_mustache') . ' Error: ' . $e->getMessage(),
];
}
// Validate based on content type
if (strtoupper($type) === 'JSON') {
// Clear any previous JSON errors
json_decode('{}'); // Clear state
// JSON validation
$decoded = json_decode($content);
$jsonError = json_last_error();
if ($jsonError !== JSON_ERROR_NONE) {
return [
'valid' => false,
'error' => _l('einvoice_template_invalid_json') . ' Error: ' . json_last_error_msg(),
];
}
// Check if content is just whitespace or empty after decoding
if ($decoded === null && trim($content) !== 'null') {
return [
'valid' => false,
'error' => _l('einvoice_template_invalid_json'),
];
}
} else {
// XML validation (default)
$dom = new DOMDocument();
// Suppress errors with @ to allow controlled error handling
libxml_use_internal_errors(true);
$isValid = $dom->loadXML($content);
if (! $isValid) {
$errors = libxml_get_errors();
$errorMessage = _l('einvoice_template_invalid_xml');
if (! empty($errors)) {
$errorMessage .= ' Error: ' . $errors[0]->message;
}
libxml_clear_errors();
return [
'valid' => false,
'error' => $errorMessage,
];
}
}
return [
'valid' => true,
'error' => null,
];
}
public function output(string $relType, int $relId): void
{
$this->load->model('templates_model');
$template = $this->templates_model->find(
get_option('einvoice_default_' . $relType . '_template')
);
if (! $template) {
set_alert('warning', _l('einvoice_no_template_set'));
redirect(admin_url("{$relType}s#{$relId}"));
}
$this->load->model('invoices_model');
$this->load->model('credit_notes_model');
switch ($relType) {
case 'invoice':
$model = $this->invoices_model->get($relId);
break;
case 'credit_note':
$model = $this->credit_notes_model->get($relId);
break;
default:
show_404();
return;
}
$einvoiceData = match ($relType) {
'invoice' => new Perfexcrm\EInvoice\Data\Invoice($model),
'credit_note' => new Perfexcrm\EInvoice\Data\CreditNote($model),
};
$handler = new EinvoiceHandler();
// Determine output format based on template content type
$fileExtension = strtoupper($template->content_type) === 'JSON' ? 'json' : 'xml';
$output = $handler->renderTemplate($template->content, $einvoiceData, $template->content_type);
/** TODO: use invoice/credit note number formatted as filename */
$filename = "{$relType}_{$relId}.{$fileExtension}";
if ($this->input->get('output_type') === 'view') {
OutputWriter::stream($filename, $output, $template->content_type);
} else {
OutputWriter::download($filename, $output, $template->content_type);
}
}
public function export(): void
{
if (staff_cant('bulk_export', 'einvoice_module')) {
access_denied();
}
if ($this->input->post()) {
$exportType = $this->input->post('export_type');
$hasPermission = match ($exportType) {
'invoice' => staff_can('view', 'invoices'),
'credit_note' => staff_can('view', 'credit_notes'),
default => false,
};
$config = new BulkExporterConfig(
$exportType,
$hasPermission,
$this->input->post('date-from'),
$this->input->post('date-to'),
$this->input->post($exportType . 's_export_status'),
);
$bulkExporter = new BulkExporter($config);
$bulkExporter->export();
return;
}
$data['features'] = [
'invoice' => _l('invoices'),
'credit_note' => _l('credit_notes'),
];
$data['title'] = _l('einvoice_module_bulk_export');
$data['invoiceStatuses'] = $this->invoices_model->get_statuses();
$data['creditNoteStatuses'] = $this->credit_notes_model->get_statuses();
$this->load->view('einvoice/bulk_export', $data);
}
}