diff --git a/app/services/subscription_matrix.py b/app/services/subscription_matrix.py index e3444d8..82a6a21 100644 --- a/app/services/subscription_matrix.py +++ b/app/services/subscription_matrix.py @@ -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) - - # 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 - ): + has_period = line_has_period_field or bool(line_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 diff --git a/find_ar2_customer.py b/find_ar2_customer.py new file mode 100644 index 0000000..1c3b37a --- /dev/null +++ b/find_ar2_customer.py @@ -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()) diff --git a/search_all_ar2.py b/search_all_ar2.py new file mode 100644 index 0000000..c4ac37e --- /dev/null +++ b/search_all_ar2.py @@ -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())