<?php
/**
 * EN 16931 invoice data assembly + CII and UBL 2.1 XML emitters.
 *
 * Profile: EN 16931 (Factur-X "EN 16931" profile / UBL CIUS). French specifics:
 * SIREN as legal registration (schemeID 0002) for both parties, per-VAT-rate
 * breakdown (BG-23) with category codes, UNTDID 4461 payment means, due date,
 * delivery date, catégorie d'opération + option-débits mentions (BT-22 notes).
 * Amounts are EUR, 2 decimals. Multi-currency stores are warned in the
 * readiness checklist and unsupported in v1.
 *
 * Standalone CII/UBL XML is legally valid under the French reform (socle
 * minimal) — every Plateforme Agréée must accept it.
 *
 * @package FEPW
 */

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

/**
 * Class FEPW_Builder
 */
class FEPW_Builder {

	/**
	 * Assemble the normalized invoice-data array from an order (or refund).
	 *
	 * @param WC_Order                 $order      Parent order.
	 * @param string                   $doc_number Document number.
	 * @param string                   $doc_type   'invoice'|'credit_note'.
	 * @param WC_Order_Refund|null     $refund     Refund when credit note.
	 * @param string                   $ref_number Referenced invoice number (credit notes, BT-25).
	 * @return array
	 */
	public static function assemble( $order, $doc_number, $doc_type = 'invoice', $refund = null, $ref_number = '' ) {
		$settings = get_option( 'fepw_settings', array() );
		$buyer    = FEPW_Checkout::get_order_fields( $order );
		$source   = $refund ? $refund : $order;
		$sign     = $refund ? -1 : 1; // Refund totals are negative; credit notes carry positive amounts.

		// Per-rate VAT breakdown (BG-23): aggregate line + shipping + fee taxes by rate percent.
		$breakdown = array();
		$add = function ( $rate_pct, $base, $tax ) use ( &$breakdown ) {
			$key = number_format( (float) $rate_pct, 2, '.', '' );
			if ( ! isset( $breakdown[ $key ] ) ) {
				$breakdown[ $key ] = array(
					'rate'     => (float) $rate_pct,
					'base'     => 0.0,
					'tax'      => 0.0,
					'category' => ( (float) $rate_pct > 0 ) ? 'S' : 'Z',
				);
			}
			$breakdown[ $key ]['base'] += (float) $base;
			$breakdown[ $key ]['tax']  += (float) $tax;
		};

		$lines    = array();
		$line_num = 0;
		foreach ( $source->get_items( array( 'line_item', 'shipping', 'fee' ) ) as $item ) {
			$line_num++;
			$total = $sign * (float) $item->get_total();
			$tax   = $sign * (float) $item->get_total_tax();

			// Effective rate for this line (from Woo tax data when present).
			$rate_pct = 0.0;
			$taxes    = $item->get_taxes();
			if ( ! empty( $taxes['total'] ) ) {
				foreach ( $taxes['total'] as $rate_id => $tax_amount ) {
					if ( '' === $tax_amount || null === $tax_amount ) {
						continue;
					}
					$rate_pct = (float) WC_Tax::get_rate_percent_value( $rate_id );
					break;
				}
			}
			$add( $rate_pct, $total, $tax );

			$qty = 1;
			if ( is_callable( array( $item, 'get_quantity' ) ) && (int) $item->get_quantity() > 0 ) {
				$qty = (int) $item->get_quantity();
			}
			$lines[] = array(
				'id'       => $line_num,
				'name'     => $item->get_name(),
				'qty'      => $qty,
				'unit_net' => $qty > 0 ? $total / $qty : $total,
				'net'      => $total,
				'rate'     => $rate_pct,
				'category' => $rate_pct > 0 ? 'S' : 'Z',
			);
		}

		$total_ht  = 0.0;
		$total_tva = 0.0;
		foreach ( $breakdown as $b ) {
			$total_ht  += $b['base'];
			$total_tva += $b['tax'];
		}

		// Amount-only refunds (no line detail) produce zero item totals: fall back
		// to one synthetic line at the ORDER's dominant VAT rate so the avoir
		// carries the real refunded amount. Line-detailed refunds take the precise path.
		if ( $refund && abs( $total_ht ) < 0.005 && abs( (float) $refund->get_amount() ) > 0.005 ) {
			$dominant_rate = 0.0;
			$dominant_base = -1.0;
			foreach ( FEPW_Builder::assemble( $order, '~probe~', 'invoice' )['breakdown'] as $ob ) {
				if ( $ob['base'] > $dominant_base ) {
					$dominant_base = $ob['base'];
					$dominant_rate = $ob['rate'];
				}
			}
			$ttc = (float) $refund->get_amount();
			$ht  = $dominant_rate > 0 ? round( $ttc / ( 1 + $dominant_rate / 100 ), 2 ) : $ttc;
			$tva = round( $ttc - $ht, 2 );

			$lines     = array(
				array(
					'id'       => 1,
					'name'     => __( 'Refund (remboursement)', 'buildsbyluke-facture-electronique-for-woocommerce' ),
					'qty'      => 1,
					'unit_net' => $ht,
					'net'      => $ht,
					'rate'     => $dominant_rate,
					'category' => $dominant_rate > 0 ? 'S' : 'Z',
				),
			);
			$breakdown = array(
				number_format( $dominant_rate, 2, '.', '' ) => array(
					'rate'     => $dominant_rate,
					'base'     => $ht,
					'tax'      => $tva,
					'category' => $dominant_rate > 0 ? 'S' : 'Z',
				),
			);
			$total_ht  = $ht;
			$total_tva = $tva;
		}

		// UNTDID 4461 payment means from the gateway.
		$gateway = $order->get_payment_method();
		$means   = in_array( $gateway, array( 'bacs' ), true ) ? '30' : ( in_array( $gateway, array( 'stripe', 'card', 'woocommerce_payments', 'payplug' ), true ) ? '48' : 'ZZZ' );

		$issue_date = wp_date( 'Ymd' );
		$due_days   = isset( $settings['due_days'] ) ? (int) $settings['due_days'] : 30;

		$notes = array();
		if ( ! empty( $settings['option_debits'] ) ) {
			$notes[] = 'Option pour le paiement de la taxe d\'après les débits';
		}
		$category_labels = array(
			'goods'    => 'Livraison de biens',
			'services' => 'Prestation de services',
			'mixed'    => 'Opération mixte (biens et services)',
		);
		$op_cat  = isset( $settings['operation_category'] ) ? $settings['operation_category'] : 'goods';
		$notes[] = 'Catégorie d\'opération : ' . ( isset( $category_labels[ $op_cat ] ) ? $category_labels[ $op_cat ] : $op_cat );

		$paid_date = $order->get_date_paid();
		$delivery  = $paid_date ? $paid_date->date( 'Ymd' ) : $issue_date;

		return array(
			'type_code'  => 'credit_note' === $doc_type ? '381' : '380',
			'number'     => $doc_number,
			'issue_date' => $issue_date,
			'due_date'   => wp_date( 'Ymd', time() + $due_days * DAY_IN_SECONDS ),
			'delivery'   => $delivery,
			'currency'   => 'EUR',
			'notes'      => $notes,
			'ref_number' => $ref_number,
			'order_ref'  => (string) $order->get_order_number(),
			'means'      => $means,
			'seller'     => array(
				'name'    => get_option( 'blogname' ),
				'siren'   => isset( $settings['seller_siren'] ) ? $settings['seller_siren'] : '',
				'tva'     => isset( $settings['seller_tva'] ) ? $settings['seller_tva'] : '',
				'address' => WC()->countries ? WC()->countries->get_base_address() : '',
				'city'    => WC()->countries ? WC()->countries->get_base_city() : '',
				'postcode' => WC()->countries ? WC()->countries->get_base_postcode() : '',
				'country' => 'FR',
				'legal'   => trim( implode( ' — ', array_filter( array(
					isset( $settings['seller_legal_form'] ) ? $settings['seller_legal_form'] : '',
					isset( $settings['seller_rcs'] ) ? $settings['seller_rcs'] : '',
					isset( $settings['seller_capital'] ) && '' !== $settings['seller_capital'] ? 'Capital : ' . $settings['seller_capital'] : '',
				) ) ) ),
			),
			'buyer'      => array(
				'name'     => trim( $order->get_billing_company() ? $order->get_billing_company() : $order->get_formatted_billing_full_name() ),
				'siren'    => $buyer['siren'],
				'tva'      => $buyer['tva'],
				'address'  => trim( $order->get_billing_address_1() . ' ' . $order->get_billing_address_2() ),
				'city'     => $order->get_billing_city(),
				'postcode' => $order->get_billing_postcode(),
				'country'  => $order->get_billing_country() ? $order->get_billing_country() : 'FR',
			),
			'lines'      => $lines,
			'breakdown'  => array_values( $breakdown ),
			'total_ht'   => round( $total_ht, 2 ),
			'total_tva'  => round( $total_tva, 2 ),
			'total_ttc'  => round( $total_ht + $total_tva, 2 ),
		);
	}

	/**
	 * Format an amount for XML (2dp, dot separator).
	 *
	 * @param float $n Amount.
	 * @return string
	 */
	private static function amt( $n ) {
		return number_format( (float) $n, 2, '.', '' );
	}

	/**
	 * Emit CII (CrossIndustryInvoice) XML, EN 16931 profile.
	 *
	 * @param array $d Assembled data.
	 * @return string XML.
	 */
	public static function to_cii( $d ) {
		$rsm = 'urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100';
		$ram = 'urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100';
		$udt = 'urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100';

		$doc = new DOMDocument( '1.0', 'UTF-8' );
		$doc->formatOutput = true;

		$root = $doc->createElementNS( $rsm, 'rsm:CrossIndustryInvoice' );
		$root->setAttributeNS( 'http://www.w3.org/2000/xmlns/', 'xmlns:ram', $ram );
		$root->setAttributeNS( 'http://www.w3.org/2000/xmlns/', 'xmlns:udt', $udt );
		$doc->appendChild( $root );

		$el = function ( $ns, $name, $value = null, $attrs = array() ) use ( $doc ) {
			$node = $doc->createElementNS( $ns, $name );
			if ( null !== $value && '' !== $value ) {
				$node->appendChild( $doc->createTextNode( (string) $value ) );
			}
			foreach ( $attrs as $k => $v ) {
				$node->setAttribute( $k, $v );
			}
			return $node;
		};

		// ExchangedDocumentContext (BT-24 guideline).
		$ctx  = $el( $rsm, 'rsm:ExchangedDocumentContext' );
		$gdl  = $el( $ram, 'ram:GuidelineSpecifiedDocumentContextParameter' );
		$gdl->appendChild( $el( $ram, 'ram:ID', 'urn:cen.eu:en16931:2017' ) );
		$ctx->appendChild( $gdl );
		$root->appendChild( $ctx );

		// ExchangedDocument.
		$hdr = $el( $rsm, 'rsm:ExchangedDocument' );
		$hdr->appendChild( $el( $ram, 'ram:ID', $d['number'] ) );
		$hdr->appendChild( $el( $ram, 'ram:TypeCode', $d['type_code'] ) );
		$idt = $el( $ram, 'ram:IssueDateTime' );
		$idt->appendChild( $el( $udt, 'udt:DateTimeString', $d['issue_date'], array( 'format' => '102' ) ) );
		$hdr->appendChild( $idt );
		foreach ( $d['notes'] as $note ) {
			$n = $el( $ram, 'ram:IncludedNote' );
			$n->appendChild( $el( $ram, 'ram:Content', $note ) );
			$hdr->appendChild( $n );
		}
		$root->appendChild( $hdr );

		// SupplyChainTradeTransaction.
		$txn = $el( $rsm, 'rsm:SupplyChainTradeTransaction' );

		foreach ( $d['lines'] as $line ) {
			$li  = $el( $ram, 'ram:IncludedSupplyChainTradeLineItem' );
			$ld  = $el( $ram, 'ram:AssociatedDocumentLineDocument' );
			$ld->appendChild( $el( $ram, 'ram:LineID', $line['id'] ) );
			$li->appendChild( $ld );
			$pr = $el( $ram, 'ram:SpecifiedTradeProduct' );
			$pr->appendChild( $el( $ram, 'ram:Name', $line['name'] ) );
			$li->appendChild( $pr );
			$ag = $el( $ram, 'ram:SpecifiedLineTradeAgreement' );
			$np = $el( $ram, 'ram:NetPriceProductTradePrice' );
			$np->appendChild( $el( $ram, 'ram:ChargeAmount', self::amt( $line['unit_net'] ) ) );
			$ag->appendChild( $np );
			$li->appendChild( $ag );
			$dl = $el( $ram, 'ram:SpecifiedLineTradeDelivery' );
			$dl->appendChild( $el( $ram, 'ram:BilledQuantity', $line['qty'], array( 'unitCode' => 'C62' ) ) );
			$li->appendChild( $dl );
			$st = $el( $ram, 'ram:SpecifiedLineTradeSettlement' );
			$tx = $el( $ram, 'ram:ApplicableTradeTax' );
			$tx->appendChild( $el( $ram, 'ram:TypeCode', 'VAT' ) );
			$tx->appendChild( $el( $ram, 'ram:CategoryCode', $line['category'] ) );
			$tx->appendChild( $el( $ram, 'ram:RateApplicablePercent', self::amt( $line['rate'] ) ) );
			$st->appendChild( $tx );
			$sm = $el( $ram, 'ram:SpecifiedTradeSettlementLineMonetarySummation' );
			$sm->appendChild( $el( $ram, 'ram:LineTotalAmount', self::amt( $line['net'] ) ) );
			$st->appendChild( $sm );
			$li->appendChild( $st );
			$txn->appendChild( $li );
		}

		// Header trade agreement: parties.
		$agr = $el( $ram, 'ram:ApplicableHeaderTradeAgreement' );
		foreach ( array( 'Seller' => $d['seller'], 'Buyer' => $d['buyer'] ) as $role => $party ) {
			$pt = $el( $ram, 'ram:' . $role . 'TradeParty' );
			$pt->appendChild( $el( $ram, 'ram:Name', $party['name'] ) );
			if ( ! empty( $party['legal'] ) ) {
				$pt->appendChild( $el( $ram, 'ram:Description', $party['legal'] ) );
			}
			if ( ! empty( $party['siren'] ) ) {
				$lo = $el( $ram, 'ram:SpecifiedLegalOrganization' );
				$lo->appendChild( $el( $ram, 'ram:ID', $party['siren'], array( 'schemeID' => '0002' ) ) );
				$pt->appendChild( $lo );
			}
			$ad = $el( $ram, 'ram:PostalTradeAddress' );
			$ad->appendChild( $el( $ram, 'ram:PostcodeCode', $party['postcode'] ) );
			$ad->appendChild( $el( $ram, 'ram:LineOne', $party['address'] ) );
			$ad->appendChild( $el( $ram, 'ram:CityName', $party['city'] ) );
			$ad->appendChild( $el( $ram, 'ram:CountryID', $party['country'] ) );
			$pt->appendChild( $ad );
			if ( ! empty( $party['tva'] ) ) {
				$tr = $el( $ram, 'ram:SpecifiedTaxRegistration' );
				$tr->appendChild( $el( $ram, 'ram:ID', $party['tva'], array( 'schemeID' => 'VA' ) ) );
				$pt->appendChild( $tr );
			}
			$agr->appendChild( $pt );
		}
		$bor = $el( $ram, 'ram:BuyerOrderReferencedDocument' );
		$bor->appendChild( $el( $ram, 'ram:IssuerAssignedID', $d['order_ref'] ) );
		$agr->appendChild( $bor );
		$txn->appendChild( $agr );

		// Delivery.
		$dlv = $el( $ram, 'ram:ApplicableHeaderTradeDelivery' );
		$ev  = $el( $ram, 'ram:ActualDeliverySupplyChainEvent' );
		$oc  = $el( $ram, 'ram:OccurrenceDateTime' );
		$oc->appendChild( $el( $udt, 'udt:DateTimeString', $d['delivery'], array( 'format' => '102' ) ) );
		$ev->appendChild( $oc );
		$dlv->appendChild( $ev );
		$txn->appendChild( $dlv );

		// Settlement.
		$set = $el( $ram, 'ram:ApplicableHeaderTradeSettlement' );
		$set->appendChild( $el( $ram, 'ram:InvoiceCurrencyCode', $d['currency'] ) );
		$pm = $el( $ram, 'ram:SpecifiedTradeSettlementPaymentMeans' );
		$pm->appendChild( $el( $ram, 'ram:TypeCode', $d['means'] ) );
		$set->appendChild( $pm );
		foreach ( $d['breakdown'] as $b ) {
			$tx = $el( $ram, 'ram:ApplicableTradeTax' );
			$tx->appendChild( $el( $ram, 'ram:CalculatedAmount', self::amt( $b['tax'] ) ) );
			$tx->appendChild( $el( $ram, 'ram:TypeCode', 'VAT' ) );
			$tx->appendChild( $el( $ram, 'ram:BasisAmount', self::amt( $b['base'] ) ) );
			$tx->appendChild( $el( $ram, 'ram:CategoryCode', $b['category'] ) );
			$tx->appendChild( $el( $ram, 'ram:RateApplicablePercent', self::amt( $b['rate'] ) ) );
			$set->appendChild( $tx );
		}
		$pt = $el( $ram, 'ram:SpecifiedTradePaymentTerms' );
		$dd = $el( $ram, 'ram:DueDateDateTime' );
		$dd->appendChild( $el( $udt, 'udt:DateTimeString', $d['due_date'], array( 'format' => '102' ) ) );
		$pt->appendChild( $dd );
		$set->appendChild( $pt );
		$sum = $el( $ram, 'ram:SpecifiedTradeSettlementHeaderMonetarySummation' );
		$sum->appendChild( $el( $ram, 'ram:LineTotalAmount', self::amt( $d['total_ht'] ) ) );
		$sum->appendChild( $el( $ram, 'ram:TaxBasisTotalAmount', self::amt( $d['total_ht'] ) ) );
		$sum->appendChild( $el( $ram, 'ram:TaxTotalAmount', self::amt( $d['total_tva'] ), array( 'currencyID' => $d['currency'] ) ) );
		$sum->appendChild( $el( $ram, 'ram:GrandTotalAmount', self::amt( $d['total_ttc'] ) ) );
		$sum->appendChild( $el( $ram, 'ram:DuePayableAmount', self::amt( $d['total_ttc'] ) ) );
		$set->appendChild( $sum );
		if ( '' !== $d['ref_number'] ) {
			$ref = $el( $ram, 'ram:InvoiceReferencedDocument' );
			$ref->appendChild( $el( $ram, 'ram:IssuerAssignedID', $d['ref_number'] ) );
			$set->appendChild( $ref );
		}
		$txn->appendChild( $set );

		$root->appendChild( $txn );

		return (string) $doc->saveXML();
	}

	/**
	 * Emit UBL 2.1 XML (Invoice or CreditNote).
	 *
	 * @param array $d Assembled data.
	 * @return string XML.
	 */
	public static function to_ubl( $d ) {
		$is_credit = '381' === $d['type_code'];
		$rootns    = $is_credit
			? 'urn:oasis:names:specification:ubl:schema:xsd:CreditNote-2'
			: 'urn:oasis:names:specification:ubl:schema:xsd:Invoice-2';
		$cac = 'urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2';
		$cbc = 'urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2';

		$doc = new DOMDocument( '1.0', 'UTF-8' );
		$doc->formatOutput = true;
		$root = $doc->createElementNS( $rootns, $is_credit ? 'CreditNote' : 'Invoice' );
		$root->setAttributeNS( 'http://www.w3.org/2000/xmlns/', 'xmlns:cac', $cac );
		$root->setAttributeNS( 'http://www.w3.org/2000/xmlns/', 'xmlns:cbc', $cbc );
		$doc->appendChild( $root );

		$el = function ( $ns, $name, $value = null, $attrs = array() ) use ( $doc ) {
			$node = $doc->createElementNS( $ns, $name );
			if ( null !== $value && '' !== $value ) {
				$node->appendChild( $doc->createTextNode( (string) $value ) );
			}
			foreach ( $attrs as $k => $v ) {
				$node->setAttribute( $k, $v );
			}
			return $node;
		};
		$date = function ( $ymd ) {
			return substr( $ymd, 0, 4 ) . '-' . substr( $ymd, 4, 2 ) . '-' . substr( $ymd, 6, 2 );
		};

		$root->appendChild( $el( $cbc, 'cbc:CustomizationID', 'urn:cen.eu:en16931:2017' ) );
		$root->appendChild( $el( $cbc, 'cbc:ID', $d['number'] ) );
		$root->appendChild( $el( $cbc, 'cbc:IssueDate', $date( $d['issue_date'] ) ) );
		if ( ! $is_credit ) {
			$root->appendChild( $el( $cbc, 'cbc:DueDate', $date( $d['due_date'] ) ) );
			$root->appendChild( $el( $cbc, 'cbc:InvoiceTypeCode', '380' ) );
		} else {
			$root->appendChild( $el( $cbc, 'cbc:CreditNoteTypeCode', '381' ) );
		}
		foreach ( $d['notes'] as $note ) {
			$root->appendChild( $el( $cbc, 'cbc:Note', $note ) );
		}
		$root->appendChild( $el( $cbc, 'cbc:DocumentCurrencyCode', $d['currency'] ) );

		$oref = $el( $cac, 'cac:OrderReference' );
		$oref->appendChild( $el( $cbc, 'cbc:ID', $d['order_ref'] ) );
		$root->appendChild( $oref );

		if ( '' !== $d['ref_number'] ) {
			$bref = $el( $cac, 'cac:BillingReference' );
			$iref = $el( $cac, 'cac:InvoiceDocumentReference' );
			$iref->appendChild( $el( $cbc, 'cbc:ID', $d['ref_number'] ) );
			$bref->appendChild( $iref );
			$root->appendChild( $bref );
		}

		foreach ( array( 'AccountingSupplierParty' => $d['seller'], 'AccountingCustomerParty' => $d['buyer'] ) as $wrap => $party ) {
			$sp = $el( $cac, 'cac:' . $wrap );
			$p  = $el( $cac, 'cac:Party' );
			$pn = $el( $cac, 'cac:PartyName' );
			$pn->appendChild( $el( $cbc, 'cbc:Name', $party['name'] ) );
			$p->appendChild( $pn );
			$pa = $el( $cac, 'cac:PostalAddress' );
			$pa->appendChild( $el( $cbc, 'cbc:StreetName', $party['address'] ) );
			$pa->appendChild( $el( $cbc, 'cbc:CityName', $party['city'] ) );
			$pa->appendChild( $el( $cbc, 'cbc:PostalZone', $party['postcode'] ) );
			$co = $el( $cac, 'cac:Country' );
			$co->appendChild( $el( $cbc, 'cbc:IdentificationCode', $party['country'] ) );
			$pa->appendChild( $co );
			$p->appendChild( $pa );
			if ( ! empty( $party['tva'] ) ) {
				$ts = $el( $cac, 'cac:PartyTaxScheme' );
				$ts->appendChild( $el( $cbc, 'cbc:CompanyID', $party['tva'] ) );
				$sch = $el( $cac, 'cac:TaxScheme' );
				$sch->appendChild( $el( $cbc, 'cbc:ID', 'VAT' ) );
				$ts->appendChild( $sch );
				$p->appendChild( $ts );
			}
			$le = $el( $cac, 'cac:PartyLegalEntity' );
			$le->appendChild( $el( $cbc, 'cbc:RegistrationName', $party['name'] ) );
			if ( ! empty( $party['siren'] ) ) {
				$le->appendChild( $el( $cbc, 'cbc:CompanyID', $party['siren'], array( 'schemeID' => '0002' ) ) );
			}
			$p->appendChild( $le );
			$sp->appendChild( $p );
			$root->appendChild( $sp );
		}

		$pm = $el( $cac, 'cac:PaymentMeans' );
		$pm->appendChild( $el( $cbc, 'cbc:PaymentMeansCode', $d['means'] ) );
		$root->appendChild( $pm );

		$tt = $el( $cac, 'cac:TaxTotal' );
		$tt->appendChild( $el( $cbc, 'cbc:TaxAmount', self::amt( $d['total_tva'] ), array( 'currencyID' => $d['currency'] ) ) );
		foreach ( $d['breakdown'] as $b ) {
			$ts = $el( $cac, 'cac:TaxSubtotal' );
			$ts->appendChild( $el( $cbc, 'cbc:TaxableAmount', self::amt( $b['base'] ), array( 'currencyID' => $d['currency'] ) ) );
			$ts->appendChild( $el( $cbc, 'cbc:TaxAmount', self::amt( $b['tax'] ), array( 'currencyID' => $d['currency'] ) ) );
			$tc = $el( $cac, 'cac:TaxCategory' );
			$tc->appendChild( $el( $cbc, 'cbc:ID', $b['category'] ) );
			$tc->appendChild( $el( $cbc, 'cbc:Percent', self::amt( $b['rate'] ) ) );
			$sch = $el( $cac, 'cac:TaxScheme' );
			$sch->appendChild( $el( $cbc, 'cbc:ID', 'VAT' ) );
			$tc->appendChild( $sch );
			$ts->appendChild( $tc );
			$tt->appendChild( $ts );
		}
		$root->appendChild( $tt );

		$lmt = $el( $cac, 'cac:LegalMonetaryTotal' );
		$lmt->appendChild( $el( $cbc, 'cbc:LineExtensionAmount', self::amt( $d['total_ht'] ), array( 'currencyID' => $d['currency'] ) ) );
		$lmt->appendChild( $el( $cbc, 'cbc:TaxExclusiveAmount', self::amt( $d['total_ht'] ), array( 'currencyID' => $d['currency'] ) ) );
		$lmt->appendChild( $el( $cbc, 'cbc:TaxInclusiveAmount', self::amt( $d['total_ttc'] ), array( 'currencyID' => $d['currency'] ) ) );
		$lmt->appendChild( $el( $cbc, 'cbc:PayableAmount', self::amt( $d['total_ttc'] ), array( 'currencyID' => $d['currency'] ) ) );
		$root->appendChild( $lmt );

		$line_tag = $is_credit ? 'CreditNoteLine' : 'InvoiceLine';
		$qty_tag  = $is_credit ? 'CreditedQuantity' : 'InvoicedQuantity';
		foreach ( $d['lines'] as $line ) {
			$ln = $el( $cac, 'cac:' . $line_tag );
			$ln->appendChild( $el( $cbc, 'cbc:ID', $line['id'] ) );
			$ln->appendChild( $el( $cbc, 'cbc:' . $qty_tag, $line['qty'], array( 'unitCode' => 'C62' ) ) );
			$ln->appendChild( $el( $cbc, 'cbc:LineExtensionAmount', self::amt( $line['net'] ), array( 'currencyID' => $d['currency'] ) ) );
			$it = $el( $cac, 'cac:Item' );
			$it->appendChild( $el( $cbc, 'cbc:Name', $line['name'] ) );
			$ct = $el( $cac, 'cac:ClassifiedTaxCategory' );
			$ct->appendChild( $el( $cbc, 'cbc:ID', $line['category'] ) );
			$ct->appendChild( $el( $cbc, 'cbc:Percent', self::amt( $line['rate'] ) ) );
			$sch = $el( $cac, 'cac:TaxScheme' );
			$sch->appendChild( $el( $cbc, 'cbc:ID', 'VAT' ) );
			$ct->appendChild( $sch );
			$it->appendChild( $ct );
			$ln->appendChild( $it );
			$pr = $el( $cac, 'cac:Price' );
			$pr->appendChild( $el( $cbc, 'cbc:PriceAmount', self::amt( $line['unit_net'] ), array( 'currencyID' => $d['currency'] ) ) );
			$ln->appendChild( $pr );
			$root->appendChild( $ln );
		}

		return (string) $doc->saveXML();
	}
}
