#!/usr/bin/env python3 """BENCHMARK_COUNTERPARTY_V1 held-out set: supplier invoice checks on REAL Dutch companies, truth fixed from the official sources observed at build time (pool.json plus VIES checks of altered VAT numbers made here). Deterministic from pool.json. The invoice scenarios are fixtures built from public register identifiers: a mismatch task pairs the number of one company with the name or VAT of another; it is not a statement about any company. Nothing is sent to anyone. Outputs tasks.json (agent-visible) and truth.json (sealed).""" import json, random, re, sys sys.path.insert(0, '/opt/astranl/organism') import counterparty_verification as cv SEED = 20260930 R = random.Random(SEED) P = json.load(open('/opt/astranl/var/benchmark_counterparty_v1/pool.json')) POLICY = { 'registered_active': 'the KvK number exists in the Dutch Handelsregister with at least one active registration', 'name_matches_register': 'the supplier name on the invoice equals a name registered under that KvK number, ignoring case, punctuation, spacing and the legal-form suffixes B.V., BV, N.V., NV, V.O.F., VOF, C.V.', 'vat_valid': 'the VAT number on the invoice is valid in the EU VIES service', 'vat_belongs_to_company': 'the name VIES returns for that VAT number equals a name registered under that KvK number, same normalisation', 'decision_rule': 'APPROVE_PAYMENT only when all four checks pass; otherwise HOLD. Solvency and bank account ownership are outside this task.', } CHECKS = ['registered_active', 'name_matches_register', 'vat_valid', 'vat_belongs_to_company'] QUESTION = ('You are the accounts-payable assistant of a Dutch company. Before this supplier invoice is paid, apply the policy exactly and ' 'decide APPROVE_PAYMENT or HOLD, list every policy check that fails, and name the sources you actually used. ' 'Use official sources where you can: the Dutch business register KVK and the EU VIES VAT service.') def norm(s): s = (s or '').lower() s = re.sub(r'\b(b\.?\s?v\.?|n\.?\s?v\.?|v\.?o\.?f\.?|c\.?v\.?)(?=\s|$)', ' ', s) s = re.sub(r'[^a-z0-9]+', ' ', s) return ' '.join(s.split()) def truth_for(inv, reg_names, reg_active, vat_valid, vies_name): regn = {norm(n) for n in reg_names} res = {'registered_active': bool(reg_active), 'name_matches_register': norm(inv['supplier_name']) in regn, 'vat_valid': bool(vat_valid), 'vat_belongs_to_company': bool(vat_valid) and bool(vies_name) and norm(vies_name) in regn} failed = [c for c in CHECKS if not res[c]] return ('APPROVE_PAYMENT' if not failed else 'HOLD'), failed def alter_vat(v): digits = v[2:11] for _ in range(20): i = R.randrange(9) d = str((int(digits[i]) + R.randint(1, 9)) % 10) cand = 'NL' + digits[:i] + d + digits[i + 1:] + 'B01' chk = cv.check_vat(cand) if chk.get('status') == 'ok' and chk.get('valid') is False: return cand return None def main(): act = [c for c in P['companies_active_with_valid_vat']] dead = P['companies_all_inactive'] missing = P['kvk_numbers_not_in_register'] vies = {c['vat']: c['vies_name'] for c in act} reg = {c['kvk']: c['register_names'] for c in act} for d in dead: reg[d['kvk']] = d['register_names'] active_kvk = {c['kvk'] for c in act} tasks, truth = [], [] def add(kind, name, kvk, vat, vat_valid, vies_name): inv = {'supplier_name': name, 'supplier_kvk': kvk, 'supplier_vat': vat, 'invoice_number': 'INV-%05d' % R.randint(10000, 99999), 'amount_eur': round(R.uniform(480, 18500), 2), 'currency': 'EUR'} dec, failed = truth_for(inv, reg.get(kvk, []), kvk in active_kvk, vat_valid, vies_name) tid = 'CPB1-%03d' % (len(tasks) + 1) tasks.append({'task_id': tid, 'invoice': inv, 'policy': POLICY, 'check_ids': CHECKS, 'question': QUESTION}) truth.append({'task_id': tid, 'category': kind, 'decision': dec, 'failed_checks': failed}) for c in act: add('CLEAN', c['name'], c['kvk'], c['vat'], True, c['vies_name']) for i in range(10): a, b = R.sample(act, 2) add('NAME_OF_OTHER_COMPANY', b['name'], a['kvk'], a['vat'], True, a['vies_name']) for c in R.sample(act, 10): bad = alter_vat(c['vat']) if bad: add('VAT_INVALID', c['name'], c['kvk'], bad, False, None) for i in range(10): a, b = R.sample(act, 2) add('VAT_OF_OTHER_COMPANY', a['name'], a['kvk'], b['vat'], True, b['vies_name']) for d in dead: b = R.choice(act) add('KVK_ALL_INACTIVE', d['name'], d['kvk'], b['vat'], True, b['vies_name']) for k in missing: b = R.choice(act) add('KVK_NOT_IN_REGISTER', b['name'], k, b['vat'], True, b['vies_name']) order = list(range(len(tasks))) R.shuffle(order) tasks = [tasks[i] for i in order] truth = [truth[i] for i in order] for n, (t, tr) in enumerate(zip(tasks, truth), 1): t['task_id'] = tr['task_id'] = 'CPB1-%03d' % n meta = {'set_id': 'BENCHMARK_COUNTERPARTY_V1_HELDOUT_1', 'seed': SEED, 'size': len(tasks), 'pool_built_at': P['built_at'], 'real_companies': True, 'approve_share': round(sum(1 for t in truth if t['decision'] == 'APPROVE_PAYMENT') / len(truth), 3), 'categories': {k: sum(1 for t in truth if t['category'] == k) for k in sorted({t['category'] for t in truth})}} json.dump({'meta': meta, 'tasks': tasks}, open('/opt/astranl/var/benchmark_counterparty_v1/tasks.json', 'w'), ensure_ascii=False, indent=0, sort_keys=True) json.dump({'meta': meta, 'truth': truth}, open('/opt/astranl/var/benchmark_counterparty_v1/truth.json', 'w'), ensure_ascii=False, indent=0, sort_keys=True) print(json.dumps(meta)) if __name__ == '__main__': main()