PHP code example of azaharizaman / nexus-tax

1. Go to this page and download the library: Download azaharizaman/nexus-tax library. Choose the download type require.

2. Extract the ZIP file and open the index.php.

3. Add this code to the index.php.
    
        
<?php
require_once('vendor/autoload.php');

/* Start to develop here. Best regards https://php-download.com/ */

    

azaharizaman / nexus-tax example snippets


// ❌ WRONG - No effective date
$rate = $rateRepository->findRateByCode('SR');

// ✅ CORRECT - Temporal query
$rate = $rateRepository->findRateByCode('SR', new \DateTimeImmutable('2024-10-15'));

TaxRate::create([
    'code' => 'HOLIDAY_2024',
    'rate' => '0.00', // 0% during holiday
    'effectiveStartDate' => new \DateTimeImmutable('2024-08-01'),
    'effectiveEndDate' => new \DateTimeImmutable('2024-08-15'),
]);

readonly class ExemptionCertificate
{
    public float $exemptionPercentage; // 0.0 to 100.0
    public string $storageKey; // Reference to PDF certificate in storage
}

final readonly class TaxContext
{
    public \DateTimeImmutable $transactionDate;
    public string $transactionType;           // 'sale', 'purchase', 'import'
    public ?string $serviceClassification;    // ServiceClassification enum value
    public array $shipFromAddress;            // ['country' => 'US', 'state' => 'CA', ...]
    public array $shipToAddress;
    public string $customerType;              // 'B2B', 'B2C', 'Government'
    public string $itemCategory;              // 'Goods', 'Services', 'Digital'
}

final readonly class TaxRate
{
    public string $rate;                      // BCMath string '5.00'
    public string $code;                      // 'SR', 'ZEROR', 'GST_5'
    public TaxType $type;                     // Enum: VAT, GST, SST, etc.
    public TaxLevel $level;                   // Enum: Federal, State, Local
    public int $applicationOrder;             // 1, 2, 3 for compound taxes
    public string $glAccountCode;             // '2200.01.MY.SST'
    public \DateTimeImmutable $effectiveStartDate;
    public ?\DateTimeImmutable $effectiveEndDate;
}

final readonly class TaxBreakdown
{
    /** @var TaxLine[] */
    public array $lines;                      // Hierarchical tax lines
    public Money $totalTaxAmount;             // Sum of all tax
    public Money $netAmount;                  // Original amount before tax
    public Money $grossAmount;                // Net + Tax
    public TaxCalculationMethod $calculationMethod;
    public bool $isReverseCharge;
}

final readonly class TaxLine
{
    public TaxRate $taxRate;
    public Money $taxableBase;                // Amount this rate applies to
    public Money $taxAmount;                  // Calculated tax
    public TaxLevel $level;
    public string $glAccountCode;
    /** @var TaxLine[] */
    public array $children;                   // Nested tax lines for compound taxes
}

final readonly class ExemptionCertificate
{
    public string $certificateId;
    public string $customerId;
    public TaxExemptionReason $reason;
    public float $exemptionPercentage;        // 0.0 to 100.0
    public \DateTimeImmutable $issueDate;
    public ?\DateTimeImmutable $expirationDate;
    public string $storageKey;                // Reference to PDF in Nexus\Storage
}

final readonly class NexusThreshold
{
    public string $jurisdictionCode;          // 'US-CA', 'US-TX'
    public ?Money $revenueThreshold;          // $100,000 USD
    public ?int $transactionThreshold;        // 200 transactions
    public \DateTimeImmutable $effectiveDate;
}

enum TaxType: string
{
    case VAT = 'vat';                         // Value Added Tax (EU)
    case GST = 'gst';                         // Goods & Services Tax (Canada, Australia, Malaysia)
    case SST = 'sst';                         // Sales & Service Tax (Malaysia)
    case SalesTax = 'sales_tax';              // US State Sales Tax
    case Excise = 'excise';                   // Excise duties
    case Withholding = 'withholding';         // Withholding tax
    
    public function label(): string;
    public function isConsumptionTax(): bool;
    public function 

enum TaxCalculationMethod: string
{
    case Exclusive = 'exclusive';             // Tax added to base (US)
    case Inclusive = 'inclusive';             // Tax 

enum ServiceClassification: string
{
    case DigitalService = 'digital_service';
    case ProfessionalService = 'professional_service';
    case PhysicalService = 'physical_service';
    case Transport = 'transport';
    case Financial = 'financial';
}

interface TaxCalculatorInterface
{
    /**
     * Calculate tax for a given context and amount.
     * 
     * Application layer decides whether to persist result (preview vs finalization).
     *
     * @throws NoNexusInJurisdictionException If no economic nexus exists
     * @throws TaxRateNotFoundException If rate code invalid or not effective
     * @throws TaxCalculationException For calculation errors
     */
    public function calculate(TaxContext $context, Money $amount): TaxBreakdown;
}

interface TaxRateRepositoryInterface
{
    /**
     * Find a tax rate by code at specific effective date.
     * 
     * CRITICAL: effectiveDate parameter is MANDATORY for temporal queries.
     *
     * @throws TaxRateNotFoundException
     */
    public function findRateByCode(
        string $code,
        \DateTimeInterface $effectiveDate
    ): TaxRate;
    
    /**
     * Find all applicable rates for jurisdiction at effective date.
     * 
     * Returns UNSORTED array. TaxCalculator sorts by applicationOrder.
     *
     * @return TaxRate[]
     */
    public function findApplicableRates(
        TaxJurisdiction $jurisdiction,
        \DateTimeInterface $effectiveDate
    ): array;
}

interface TaxJurisdictionResolverInterface
{
    /**
     * Resolve tax jurisdiction from transaction context.
     * 
     * Implements place-of-supply rules based on serviceClassification.
     * 
     * CACHE-AGNOSTIC: Application layer wraps with caching decorator.
     *
     * @throws InvalidTaxJurisdictionException
     */
    public function resolve(TaxContext $context): TaxJurisdiction;
}

interface TaxNexusManagerInterface
{
    /**
     * Check if business has economic nexus in jurisdiction.
     * 
     * STATEFUL: Requires historical transaction analysis.
     * Application layer implements using database queries.
     */
    public function hasNexus(
        string $jurisdictionCode,
        \DateTimeInterface $date
    ): bool;
    
    /**
     * Get nexus threshold for jurisdiction.
     */
    public function getNexusThreshold(
        string $jurisdictionCode,
        \DateTimeInterface $date
    ): NexusThreshold;
}

interface TaxExemptionManagerInterface
{
    /**
     * Validate exemption certificate and return exemption percentage.
     * 
     * VALIDATION ONLY: Workflow (approval/revocation) in application layer.
     *
     * @return float Exemption percentage (0.0 to 100.0)
     * @throws TaxExemptionExpiredException
     * @throws InvalidExemptionPercentageException
     */
    public function validateExemption(
        string $certificateId,
        \DateTimeInterface $transactionDate
    ): float;
    
    /**
     * Get certificates expiring within specified days.
     * 
     * Application layer triggers notifications via Nexus\Notifier.
     *
     * @return ExemptionCertificate[]
     */
    public function getExpiringCertificates(\DateTimeInterface $withinDays): array;
}

interface TaxReportingInterface
{
    /**
     * Aggregate tax breakdowns for compliance reporting.
     * 
     * Converts all amounts to reporting currency (e.g., EUR for EU VAT).
     * Outputs generic structure for Nexus\Statutory format transformation.
     *
     * @param TaxBreakdown[] $breakdowns
     * @return ComplianceReportLine[]
     */
    public function aggregateForCompliance(
        array $breakdowns,
        string $reportingCurrency,
        \DateTimeInterface $periodStart,
        \DateTimeInterface $periodEnd
    ): array;
}

use Nexus\Tax\Contracts\TaxCalculatorInterface;
use Nexus\Tax\ValueObjects\TaxContext;
use Nexus\Currency\ValueObjects\Money;

// Injected via DI
private readonly TaxCalculatorInterface $taxCalculator;

public function calculateInvoiceTax(array $invoiceData): TaxBreakdown
{
    // Construct TaxContext from invoice data
    $context = new TaxContext(
        transactionDate: new \DateTimeImmutable($invoiceData['date']),
        transactionType: 'sale',
        serviceClassification: null, // Physical goods
        shipFromAddress: [
            'country' => 'MY',
            'state' => 'Selangor',
            'city' => 'Petaling Jaya',
        ],
        shipToAddress: [
            'country' => 'MY',
            'state' => 'Johor',
            'city' => 'Johor Bahru',
        ],
        customerType: 'B2C',
        itemCategory: 'Goods'
    );
    
    $amount = Money::of($invoiceData['line_total'], 'MYR');
    
    // Calculate tax (stateless operation)
    $taxBreakdown = $this->taxCalculator->calculate($context, $amount);
    
    // Returns hierarchical TaxBreakdown with:
    // - Federal SST @ 10%
    // - Total tax amount
    // - GL account codes
    
    return $taxBreakdown;
}

use Nexus\Tax\Contracts\TaxJurisdictionResolverInterface;
use Nexus\Tax\Enums\ServiceClassification;

private readonly TaxJurisdictionResolverInterface $jurisdictionResolver;

public function resolveDigitalServiceJurisdiction(array $customerData): TaxJurisdiction
{
    $context = new TaxContext(
        transactionDate: new \DateTimeImmutable(),
        transactionType: 'sale',
        serviceClassification: ServiceClassification::DigitalService->value, // KEY!
        shipFromAddress: ['country' => 'MY'], // Supplier in Malaysia
        shipToAddress: ['country' => 'GB'],   // Customer in UK
        customerType: 'B2C',
        itemCategory: 'Digital'
    );
    
    // Place-of-supply rule: Digital services taxed at DESTINATION
    $jurisdiction = $this->jurisdictionResolver->resolve($context);
    
    // Result: jurisdiction->countryCode === 'GB' (UK VAT applies)
    
    return $jurisdiction;
}

use Nexus\Tax\Contracts\TaxNexusManagerInterface;
use Nexus\Tax\Exceptions\NoNexusInJurisdictionException;

private readonly TaxNexusManagerInterface $nexusManager;

public function shouldCollectTax(string $stateCode): bool
{
    try {
        $hasNexus = $this->nexusManager->hasNexus(
            jurisdictionCode: "US-{$stateCode}",
            date: new \DateTimeImmutable()
        );
        
        if (!$hasNexus) {
            // No economic presence - don't collect tax
            return false;
        }
        
        return true;
        
    } catch (NoNexusInJurisdictionException $e) {
        // Log and skip tax collection
        $this->logger->warning("No nexus in {$stateCode}", [
            'customer_id' => $customerId,
        ]);
        return false;
    }
}

use Nexus\Tax\Contracts\TaxExemptionManagerInterface;

private readonly TaxExemptionManagerInterface $exemptionManager;

public function calculateWithExemption(
    string $certificateId,
    Money $lineAmount
): TaxBreakdown {
    // Validate certificate and get exemption percentage
    $exemptionPercentage = $this->exemptionManager->validateExemption(
        certificateId: $certificateId,
        transactionDate: new \DateTimeImmutable()
    );
    // Returns: 50.0 (50% exempt)
    
    // Reduce taxable base
    $taxableBase = $lineAmount->multiply((100 - $exemptionPercentage) / 100);
    
    // Calculate tax on reduced base
    $taxBreakdown = $this->taxCalculator->calculate($context, $taxableBase);
    
    // Example: $1000 line × 50% exempt = $500 taxable
    //          $500 × 10% tax = $50 tax (instead of $100)
    
    return $taxBreakdown;
}

use Nexus\Tax\Enums\TaxCalculationMethod;

public function handleCrossBorderB2B(array $invoiceData): TaxBreakdown
{
    $context = new TaxContext(
        transactionDate: new \DateTimeImmutable(),
        transactionType: 'sale',
        serviceClassification: ServiceClassification::ProfessionalService->value,
        shipFromAddress: ['country' => 'DE'], // Germany
        shipToAddress: ['country' => 'FR'],   // France
        customerType: 'B2B',                  // KEY: Business customer
        itemCategory: 'Services'
    );
    
    $amount = Money::of(10000, 'EUR');
    
    $taxBreakdown = $this->taxCalculator->calculate($context, $amount);
    
    // Result:
    // - taxBreakdown->isReverseCharge === true
    // - taxBreakdown->totalTaxAmount === Money::of(0, 'EUR')
    // - taxBreakdown->lines[0]->glAccountCode === '2300.VAT.DEFERRED'
    
    // Invoice shows: €10,000 + €0 VAT (Reverse Charge)
    // Customer self-assesses French VAT
    
    return $taxBreakdown;
}

use Nexus\Tax\Contracts\TaxReportingInterface;

private readonly TaxReportingInterface $taxReporting;

public function generateVATReturn(
    \DateTimeInterface $periodStart,
    \DateTimeInterface $periodEnd
): array {
    // Fetch historical tax breakdowns from audit log
    $breakdowns = $this->taxAuditLogRepository->findByPeriod($periodStart, $periodEnd);
    
    // Aggregate and convert to reporting currency
    $reportLines = $this->taxReporting->aggregateForCompliance(
        breakdowns: $breakdowns,
        reportingCurrency: 'EUR', // EU VAT returns in EUR
        periodStart: $periodStart,
        periodEnd: $periodEnd
    );
    
    // Result: ComplianceReportLine[] with amounts in EUR
    // [
    //   {formFieldId: 'Box 1', taxType: 'VAT', totalAmount: Money(45000, 'EUR')},
    //   {formFieldId: 'Box 6', taxType: 'VAT', totalAmount: Money(9000, 'EUR')},
    // ]
    
    // Pass to Nexus\Statutory for XBRL transformation
    
    return $reportLines;
}

/**
 * IMMUTABLE AUDIT LOG PATTERN
 * 
 * Original transaction tax was incorrect. Create correction.
 */
public function correctTaxCalculation(string $originalTransactionId): void
{
    // 1. Fetch original tax result (immutable)
    $originalResult = $this->taxAuditLog->findByTransactionId($originalTransactionId);
    
    // 2. Create contra-transaction with NEGATIVE amount
    $correctionContext = new TaxContext(
        transactionDate: new \DateTimeImmutable(), // Today
        transactionType: 'adjustment',
        serviceClassification: $originalResult->serviceClassification,
        shipFromAddress: $originalResult->shipFromAddress,
        shipToAddress: $originalResult->shipToAddress,
        customerType: $originalResult->customerType,
        itemCategory: $originalResult->itemCategory
    );
    
    // NEGATIVE amount to reverse original tax
    $negativeAmount = $originalResult->netAmount->negate();
    
    $correctionBreakdown = $this->taxCalculator->calculate(
        $correctionContext,
        $negativeAmount
    );
    
    // 3. Persist as NEW audit log entry (not UPDATE)
    $this->taxAuditLog->create([
        'transaction_id' => $this->generateTransactionId(),
        'original_transaction_id' => $originalTransactionId,
        'transaction_type' => 'tax_adjustment',
        'tax_breakdown_json' => json_encode($correctionBreakdown),
        'total_tax_amount' => $correctionBreakdown->totalTaxAmount->negate(),
    ]);
    
    // 4. Calculate corrected tax with new context
    $correctedAmount = Money::of(1200, 'MYR'); // Corrected amount
    $correctedBreakdown = $this->taxCalculator->calculate(
        $correctedContext,
        $correctedAmount
    );
    
    // 5. Persist corrected calculation
    $this->taxAuditLog->create([
        'transaction_id' => $this->generateTransactionId(),
        'original_transaction_id' => $originalTransactionId,
        'transaction_type' => 'tax_recalculation',
        'tax_breakdown_json' => json_encode($correctedBreakdown),
    ]);
    
    // Audit trail: Original + Reversal + Corrected = Complete history
}

/**
 * APPLICATION LAYER DECISION
 * 
 * Same calculate() method, different persistence strategy.
 */
public function previewTax(array $quoteData): TaxBreakdown
{
    $context = $this->buildTaxContext($quoteData);
    $amount = Money::of($quoteData['total'], $quoteData['currency']);
    
    // Calculate tax (stateless)
    $taxBreakdown = $this->taxCalculator->calculate($context, $amount);
    
    // SKIP audit log persistence for preview
    // (no call to $this->taxAuditLog->create())
    
    return $taxBreakdown;
}

public function finalizeTax(array $invoiceData): TaxBreakdown
{
    $context = $this->buildTaxContext($invoiceData);
    $amount = Money::of($invoiceData['total'], $invoiceData['currency']);
    
    // Same calculate() method
    $taxBreakdown = $this->taxCalculator->calculate($context, $amount);
    
    // PERSIST to audit log for finalized invoice
    $this->taxAuditLog->create([
        'transaction_id' => $invoiceData['id'],
        'tax_breakdown_json' => json_encode($taxBreakdown),
        'total_tax_amount' => $taxBreakdown->totalTaxAmount,
    ]);
    
    // Publish event for data warehouse sync
    $this->eventDispatcher->dispatch(new TaxCalculatedEvent($taxBreakdown));
    
    return $taxBreakdown;
}

// Application Layer: App\Services\Sales\SalesTaxAdapter

use Nexus\Sales\Contracts\TaxCalculatorInterface as SalesTaxCalculatorInterface;
use Nexus\Tax\Contracts\TaxCalculatorInterface;
use Nexus\Tax\ValueObjects\TaxContext;

/**
 * Adapter bridging Sales package to Tax engine.
 */
final readonly class SalesTaxAdapter implements SalesTaxCalculatorInterface
{
    public function __construct(
        private TaxCalculatorInterface $taxCalculator,
        private PartyRepositoryInterface $partyRepository
    ) {}
    
    public function calculateLineTax(
        string $tenantId,
        string $productVariantId,
        float $lineSubtotal,
        string $customerId,
        string $currencyCode
    ): float {
        // Fetch customer for address data
        $customer = $this->partyRepository->findById($customerId);
        
        // Construct TaxContext from Sales domain
        $context = new TaxContext(
            transactionDate: new \DateTimeImmutable(),
            transactionType: 'sale',
            serviceClassification: null, // Assume goods
            shipFromAddress: $this->getWarehouseAddress(),
            shipToAddress: $customer->getBillingAddress()->toArray(),
            customerType: $customer->isBusinessEntity() ? 'B2B' : 'B2C',
            itemCategory: 'Goods'
        );
        
        $amount = Money::of($lineSubtotal, $currencyCode);
        
        // Delegate to tax engine
        $taxBreakdown = $this->taxCalculator->calculate($context, $amount);
        
        // Return flat tax amount for Sales compatibility
        return (float) $taxBreakdown->totalTaxAmount->getAmount();
    }
}

// Application Layer: App\Services\Tax\CachingJurisdictionResolver

use Nexus\Tax\Contracts\TaxJurisdictionResolverInterface;
use Psr\Cache\CacheItemPoolInterface;

final readonly class CachingJurisdictionResolver implements TaxJurisdictionResolverInterface
{
    public function __construct(
        private TaxJurisdictionResolverInterface $inner,
        private CacheItemPoolInterface $cache
    ) {}
    
    public function resolve(TaxContext $context): TaxJurisdiction
    {
        // Build cache key from addresses
        $cacheKey = sprintf(
            'tax_jurisdiction_%s_%s_%s',
            $context->shipFromAddress['country'],
            $context->shipToAddress['country'],
            $context->serviceClassification ?? 'goods'
        );
        
        $cacheItem = $this->cache->getItem($cacheKey);
        
        if ($cacheItem->isHit()) {
            return $cacheItem->get();
        }
        
        // Cache miss - delegate to inner resolver
        $jurisdiction = $this->inner->resolve($context);
        
        // Cache for 24 hours
        $cacheItem->set($jurisdiction);
        $cacheItem->expiresAfter(86400);
        $this->cache->save($cacheItem);
        
        return $jurisdiction;
    }
}

// Service Provider binding
$container->bind(
    TaxJurisdictionResolverInterface::class,
    fn() => new CachingJurisdictionResolver(
        new JurisdictionResolver($geocoder),
        $cachePool
    )
);

// Application Layer: App\Services\Tax\StorageExemptionManager

use Nexus\Tax\Contracts\TaxExemptionManagerInterface;
use Nexus\Storage\Contracts\StorageInterface;

final readonly class StorageExemptionManager implements TaxExemptionManagerInterface
{
    public function __construct(
        private TaxExemptionManagerInterface $inner,
        private StorageInterface $storage
    ) {}
    
    public function validateExemption(
        string $certificateId,
        \DateTimeInterface $transactionDate
    ): float {
        // Delegate validation to core manager
        return $this->inner->validateExemption($certificateId, $transactionDate);
    }
    
    public function getExpiringCertificates(\DateTimeInterface $withinDays): array
    {
        return $this->inner->getExpiringCertificates($withinDays);
    }
    
    /**
     * Extended method: Retrieve PDF certificate from storage.
     */
    public function retrieveCertificatePDF(string $certificateId): string
    {
        $certificate = $this->certificateRepository->findById($certificateId);
        
        // Use storageKey to fetch PDF
        return $this->storage->get($certificate->storageKey);
    }
}

// Application Layer: Scheduled task
$this->scheduler->yearly(function() {
    $cutoffDate = now()->subYears(10);
    
    // Archive old records to cold storage
    $this->taxAuditLog->archiveOlderThan($cutoffDate, 'glacier-storage');
});

// After persisting to tax_audit_log table
$this->eventDispatcher->dispatch(new TaxCalculatedEvent(
    aggregateId: $taxBreakdown->transactionId,
    eventType: 'tax_calculated',
    payload: [
        'transaction_id' => $transactionId,
        'tax_breakdown' => $taxBreakdown->toArray(),
        'total_tax_amount' => $taxBreakdown->totalTaxAmount,
        'jurisdiction' => $jurisdiction->toArray(),
    ],
    occurredAt: new \DateTimeImmutable()
));

// Application Layer: Scheduled task
$this->scheduler->weekly(function() {
    $futureRates = $this->taxRateRepository->findFutureDated(
        startDate: now(),
        endDate: now()->addMonths(3)
    );
    
    foreach ($futureRates as $rate) {
        $this->notifier->send(
            recipient: '[email protected]',
            channel: 'email',
            template: 'tax.rate_change_upcoming',
            data: [
                'rate_code' => $rate->code,
                'new_rate' => $rate->rate,
                'effective_date' => $rate->effectiveStartDate,
            ]
        );
    }
});