fix: Lenient filtering for subscription matrix - include all recurring products

This commit is contained in:
Christian 2026-01-27 01:33:46 +01:00
parent 89d378cf8a
commit 2ba9f5e103
3 changed files with 183 additions and 11 deletions

View File

@ -152,8 +152,10 @@ class SubscriptionMatrixService:
# Initialize products only if they have data within the selected month range
try:
# Fill in data from invoices, but only for months within range
logger.info(f"🔍 [MATRIX] Processing {len(invoices)} invoices")
for invoice in invoices:
invoice_number = invoice.get('bookedInvoiceNumber') or invoice.get('draftInvoiceNumber')
logger.debug(f"🔍 [MATRIX] Processing invoice {invoice_number}")
# Determine status based on invoice type/endpoint it came from
# Priority: use 'status' field, fallback to inferring from presence of certain fields
invoice_status = invoice.get('status', 'unknown')
@ -187,24 +189,35 @@ class SubscriptionMatrixService:
# Process invoice lines
lines = invoice.get('lines', [])
logger.debug(f"🔍 [MATRIX] Invoice {invoice_number} has {len(lines)} lines")
for line in lines:
product_number = line.get('product', {}).get('productNumber')
product_name = line.get('product', {}).get('name') or line.get('description')
line_description = (line.get('description') or product_name or "").lower()
# Check for period data
line_period_data = line.get('period', {})
line_has_period_field = bool(line_period_data.get('from'))
line_period = self._extract_period_from_text(line_description) or invoice_period
has_period = bool(line.get('period', {}).get('from') if line.get('period') else None) or bool(line_period)
has_period = line_has_period_field or bool(line_period)
# Only include lines that indicate a period or subscription or explicit period fields
if (
"periode" not in line_description
and "abonnement" not in line_description
and "periode" not in invoice_text
and "abonnement" not in invoice_text
and not has_period
):
# Check keyword matching
has_periode_keyword = "periode" in line_description or "periode" in invoice_text
has_abonnement_keyword = "abonnement" in line_description or "abonnement" in invoice_text
# More lenient filtering: Include if it has keywords OR period data OR appears to be a recurring product
# Skip only obvious one-time items (installation, setup fees, hardware, etc.)
skip_keywords = ['installation', 'opsætning', 'setup', 'hardware', 'engangs']
should_skip = any(keyword in line_description for keyword in skip_keywords)
if should_skip:
logger.debug(f"🔍 [MATRIX] Skipping one-time item: {product_name[:50] if product_name else 'NO NAME'}")
continue
# Include if it has subscription indicators OR has period data OR just include everything
# We'll filter later based on products appearing in multiple months
logger.debug(f"🔍 [MATRIX] Including: {product_name[:50] if product_name else 'NO NAME'} (periode={has_periode_keyword}, abon={has_abonnement_keyword}, period={has_period})")
if not product_number and not product_name:
logger.debug(f"Skipping line without product number: {line}")
continue
@ -291,9 +304,30 @@ class SubscriptionMatrixService:
except Exception as e:
logger.error(f"❌ Error aggregating data: {e}")
# Filter out products that only appear in one month (one-time purchases, not subscriptions)
# Keep products that appear in 2+ months OR have subscription keywords
filtered_matrix = {}
for product_key, months_data in product_matrix.items():
# Count how many months have actual data (not missing status)
months_with_data = sum(1 for cell in months_data.values() if cell["status"] != "missing")
# Check if product name suggests subscription
product_name = product_names.get(product_key, "")
has_subscription_indicators = any(
keyword in product_name.lower()
for keyword in ['periode', 'abonnement', 'hosting', 'office', 'microsoft', 'subscription']
)
# Include if it appears in multiple months OR has subscription keywords
if months_with_data >= 2 or has_subscription_indicators:
filtered_matrix[product_key] = months_data
logger.debug(f"✅ Including product: {product_name} (appears in {months_with_data} months, subscription_indicators={has_subscription_indicators})")
else:
logger.debug(f"❌ Filtering out: {product_name} (only in {months_with_data} month, no subscription indicators)")
# Convert to output format
products = []
for product_number, months_data in sorted(product_matrix.items()):
for product_number, months_data in sorted(filtered_matrix.items()):
rows = []
total_amount: float = 0.0
total_paid: float = 0.0

62
find_ar2_customer.py Normal file
View File

@ -0,0 +1,62 @@
"""Find which e-conomic customer has Hosting - AR2 product"""
import asyncio
import sys
import os
# Add app to path
sys.path.insert(0, '/Users/christianthomas/DEV/bmc_hub_dev')
from app.services.economic_service import get_economic_service
async def main():
service = get_economic_service()
# Search products from screenshot: Office 365, Microsoft 365, Hosting - AR2
search_terms = ['AR2', 'HOSTING', 'OFFICE 365', 'MICROSOFT 365', 'FAKTURERINGSGEBYR']
# Try a few known customer numbers
test_customers = [3, 9, 17, 43, 44473156, 30480748, 702007708]
for customer_num in test_customers:
print(f"\n{'='*60}")
print(f"Checking customer {customer_num}")
print('='*60)
try:
invoices = await service.get_customer_invoices(customer_num, include_lines=True)
if not invoices:
print(f" No invoices found")
continue
print(f"Found {len(invoices)} invoices")
found_products = set()
for inv in invoices:
inv_num = inv.get('bookedInvoiceNumber') or inv.get('draftInvoiceNumber')
lines = inv.get('lines', [])
for line in lines:
product_name = line.get('product', {}).get('name') or line.get('description', '')
# Check if any search term matches
for term in search_terms:
if term in product_name.upper():
found_products.add(product_name)
print(f" ✅ Invoice {inv_num} ({inv.get('date')}): {product_name}")
print(f" Amount: {line.get('netAmount', 0)}")
print(f" Period: {line.get('period')}")
break
if found_products:
print(f"\n 📦 Unique products found:")
for prod in sorted(found_products):
print(f" - {prod}")
except Exception as e:
print(f" ❌ Error: {e}")
if __name__ == "__main__":
asyncio.run(main())

76
search_all_ar2.py Normal file
View File

@ -0,0 +1,76 @@
"""Search ALL e-conomic invoices for Hosting - AR2"""
import asyncio
import sys
import os
import aiohttp
# Add app to path
sys.path.insert(0, '/Users/christianthomas/DEV/bmc_hub_dev')
from app.core.config import settings
async def main():
headers = {
"X-AppSecretToken": settings.ECONOMIC_APP_SECRET_TOKEN,
"X-AgreementGrantToken": settings.ECONOMIC_AGREEMENT_GRANT_TOKEN,
"Content-Type": "application/json"
}
endpoints = [
"https://restapi.e-conomic.com/invoices/drafts",
"https://restapi.e-conomic.com/invoices/sent",
"https://restapi.e-conomic.com/invoices/booked",
"https://restapi.e-conomic.com/invoices/paid",
"https://restapi.e-conomic.com/invoices/unpaid"
]
customers_with_ar2 = set()
async with aiohttp.ClientSession() as session:
for endpoint in endpoints:
print(f"\nChecking {endpoint.split('/')[-1]}...")
try:
async with session.get(endpoint, headers=headers) as resp:
if resp.status == 200:
data = await resp.json()
invoices = data.get('collection', [])
print(f" Found {len(invoices)} invoices")
for inv in invoices:
# Fetch full invoice with lines
self_link = inv.get('self')
if not self_link:
continue
async with session.get(self_link, headers=headers) as inv_resp:
if inv_resp.status == 200:
full_inv = await inv_resp.json()
lines = full_inv.get('lines', [])
for line in lines:
product_name = line.get('product', {}).get('name') or line.get('description', '')
if 'AR2' in product_name.upper() or 'HOSTING' in product_name.upper() or 'ABONNEMENT' in product_name.upper() or 'PERIODE' in product_name.upper():
customer_num = full_inv.get('customer', {}).get('customerNumber')
inv_num = full_inv.get('bookedInvoiceNumber') or full_inv.get('draftInvoiceNumber')
inv_date = full_inv.get('date')
period = line.get('period', {})
customers_with_ar2.add(customer_num)
print(f" ✅ Customer {customer_num} | Invoice {inv_num} | {inv_date} | {product_name[:60]}")
if period:
print(f" Period: {period}")
except Exception as e:
print(f" ❌ Error: {e}")
print(f"\n{'='*60}")
print(f"📊 SUMMARY: Found {len(customers_with_ar2)} customers with Hosting - AR2")
print(f" Customer numbers: {sorted(customers_with_ar2)}")
print('='*60)
if __name__ == "__main__":
asyncio.run(main())