whereNotNull('vat_id') ->where('vat_id', '!=', '') ->when($this->option('customer'), fn ($q, $uuid) => $q->where('uuid', $uuid)) // Longest unchecked first, so a short run covers the ones we know // least about rather than the ones checked yesterday. A plain // ascending sort does that on its own — never-checked rows are NULL // and both engines sort those first — which is why there is no raw // SQL here: MariaDB and SQLite would need different expressions for // it, and a query that only runs on one of them makes the test // database a different product from the live one. ->orderBy('vat_id_verified_at') ->limit((int) $this->option('limit')) ->get(); if ($customers->isEmpty()) { $this->info('No VAT numbers on record.'); return self::SUCCESS; } $counts = ['verified' => 0, 'rejected' => 0, 'unchecked' => 0]; foreach ($customers as $customer) { if ($this->option('dry-run')) { $this->line(sprintf('would ask: %s (%s)', $customer->normalisedVatId(), $customer->name)); continue; } $check = $verify($customer); match (true) { $check === null => null, $check->isValid() => $counts['verified']++, $check->isInvalid() => $counts['rejected']++, default => $counts['unchecked']++, }; if ($check?->isInvalid()) { // Named individually, because this one needs a person: an invoice // already issued at 0 % against a number the register now rejects // has to be looked at, and a count in a summary hides that. $this->warn(sprintf( 'not registered: %s (%s) — reverse charge no longer applies', $customer->normalisedVatId(), $customer->name, )); } } if ($this->option('dry-run')) { $this->info($customers->count().' number(s) would be asked about.'); return self::SUCCESS; } $this->info(sprintf( '%d confirmed, %d rejected, %d could not be checked (register unavailable — nothing changed for those).', $counts['verified'], $counts['rejected'], $counts['unchecked'], )); return self::SUCCESS; } }