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\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');
});