From bb6011d8ea666a934a7a3b953c87921a9f4e3c52 Mon Sep 17 00:00:00 2001 From: Luis Cuadrado Date: Wed, 12 Nov 2025 07:40:50 -0500 Subject: [PATCH] Added support of Renew Type PRs and Information about applied Adobe Flex Discounts per order --- reports.json | 4 + reports/requests/README.md | 356 +++++++++++++++++- reports/requests/entrypoint.py | 54 ++- reports/requests/templates/xlsx/template.xlsx | Bin 12192 -> 12534 bytes reports/utils.py | 117 +++++- tests/ff_requests.json | 194 ++++++++++ tests/test_request_report.py | 55 ++- tests/test_utils.py | 202 ++++++++++ 8 files changed, 950 insertions(+), 32 deletions(-) diff --git a/reports.json b/reports.json index 6baaed6..e606ed0 100644 --- a/reports.json +++ b/reports.json @@ -141,6 +141,10 @@ { "value": "adjustment", "label": "Adjustment" + }, + { + "value": "renew", + "label": "Renewal" } ] }, diff --git a/reports/requests/README.md b/reports/requests/README.md index 41fceec..ae3224d 100644 --- a/reports/requests/README.md +++ b/reports/requests/README.md @@ -1,28 +1,344 @@ -# Report Requests +# Adobe Approved Requests Report +## Overview -This report creates an Excel file with details about all approved requests with subscription scope parameters +This report creates a comprehensive Excel file with detailed information about all approved Adobe subscription requests processed through CloudBlue Connect. The report includes 53 columns covering request details, subscription information, pricing data, flex discounts, commitment terms, and more. +**Report Type**: Request-based +**Output Format**: Excel (XLSX) +**Data Source**: CloudBlue Connect API +**Total Columns**: 53 -# Available parameters +--- -Request can be parametrized by: +## Available Parameters -* Request creation date range -* Product -* Marketplace -* Environment -* Request Type +The report can be filtered using the following parameters: -# Columns -* Request ID -* Connect Subscription ID -* End Customer or External Subscription ID -* Action, Adobe Order Number, Adobe Transfer ID Number, VIP Number, and Adobe Cloud Program or Customer ID, -* Pricing SKU Level (Volume Discount level), Product Description, Part Number, Product Period, Cumulative Seat -* Order delta, Reseller ID, Reseller Name, End Customer Name End Customer External ID -* Provider ID, Provider Name, Marketplace, Product ID, Product Name, Subscription Status, Anniversary Date -* Request Effective Date, Request Creation Date, Request Type, Adobe User Email, Currency -* Cost, Reseller Cost, MSRP, Connection Type or Environment Type, Exported At +### Date Range +- **Request Creation Date Range** - Filter requests by creation date (from/to) -Command to create report: ccli report execute requests -d . \ No newline at end of file +### Filters +- **Product** - Filter by specific Adobe product(s) +- **Marketplace** - Filter by marketplace(s) +- **Environment** - Filter by connection type (production, test, preview) +- **Request Type** - Filter by request type: + - Purchase + - Change + - Suspend + - Resume + - Cancel + - Adjustment + - Renewal + +--- + +## Report Columns (53 Total) + +### Request & Subscription Identification (Columns 1-5) +1. **Request ID** - CloudBlue Connect request identifier (e.g., PR-1895-0864-1238-001) +2. **Assignee ID** - ID of the user assigned to the request (e.g., UR-841-574-187) +3. **Assignee Name** - Name of the assigned user +4. **Connect Subscription ID** - CloudBlue subscription ID (e.g., AS-1895-0864-1238) +5. **End Customer Subscription ID** - External subscription identifier + +### Adobe Order Details (Columns 6-10) +6. **Action** - Request action (Purchase, Change, Cancel, etc.) +7. **Adobe Order #** - Adobe order number from VIP/ETLA portal +8. **Adobe Transfer ID #** - Adobe transfer identifier (if applicable) +9. **VIP #** - Adobe VIP (Value Incentive Plan) number +10. **Adobe Cloud Program ID** - Adobe cloud program or customer identifier + +### Pricing & Discount Information (Columns 11-13) +11. **Pricing SKU Level (Volume Discount level)** - Formatted discount level (e.g., "Level 1", "Level 2", "TLP Level 1") +12. **Discount Group Licenses** - Raw discount group code for licenses (e.g., "01A12", "02A", "010") +13. **Discount Group Consumables** - Raw discount group code for consumables (e.g., "T1A12", "T5A12", "TBA12") + +### Product Information (Columns 14-19) +14. **Product Description** - Full product name/description +15. **Part Number** - Adobe SKU/MPN (e.g., 65322648CA) +16. **Unit of Measure** - Item type/licensing model (User, Transactions, Per Server, Units, etc.) +17. **Product Period** - Billing period (Monthly, Yearly, etc.) +18. **Cumulative Seat** - Total quantity/seat count +19. **Order Delta** - Quantity change (+/- seats) + +### Adobe Flex Discounts (Columns 20-23) +20. **Discounted MPN** - MPN(s) receiving flex discounts (comma-separated if multiple) +21. **Discounted Adobe Order Id** - Adobe order ID(s) for discounted items +22. **Adobe Discount Id** - Adobe discount identifier(s) +23. **Adobe Discount Code** - Adobe discount code(s) applied + +### Partner & Customer Information (Columns 24-27) +24. **Reseller ID** - Reseller/distributor ID +25. **Reseller Name** - Reseller/distributor name +26. **End Customer Name** - End customer company name +27. **End Customer External ID** - Customer external identifier + +### Provider & Marketplace (Columns 28-30) +28. **Provider ID** - Provider identifier +29. **Provider Name** - Provider name +30. **Marketplace** - Marketplace name + +### Product & Status (Columns 31-33) +31. **Product ID** - CloudBlue product identifier +32. **Product Name** - Product name in CloudBlue +33. **Subscription Status** - Current subscription status + +### Date Information (Columns 34-38) +34. **Anniversary Date** - Subscription anniversary/renewal date +35. **Adobe Renewal Date** - Adobe renewal date from parameters +36. **Effective Date** - Request effective date +37. **Prorata (days)** - Days between effective date and renewal date +38. **Creation Date** - Request creation timestamp + +### Request Details (Columns 39-40) +39. **Connect Order Type** - Request type (Purchase, Change, etc.) +40. **Adobe User Email** - Adobe user email address + +### Financial Information (Columns 41-44) +41. **Currency** - Currency code (USD, EUR, etc.) +42. **Cost** - Provider cost +43. **Reseller Cost** - Reseller/distributor cost +44. **MSRP** - Manufacturer's suggested retail price + +### Connection & Export (Columns 45-46) +45. **Connection Type** - Environment type (Production, Test, Preview) +46. **Exported At** - Report generation timestamp + +### Commitment Information (Columns 47-52) +47. **commitment** - Commitment status (COMMITTED, NOT_COMMITTED, etc.) +48. **commitment start date** - Start date of commitment period +49. **commitment end date** - End date of commitment period +50. **recommitment** - Recommitment status +51. **recommitment start date** - Start date of recommitment period +52. **recommitment end date** - End date of recommitment period + +### External References (Column 53) +53. **external reference id** - External reference identifier + +--- + +## Key Features + +### 1. Adobe Flex Discounts Support +The report includes comprehensive support for Adobe Flex Discounts: +- Extracts discount data from the `cb_flex_discounts_applied` parameter +- Matches discounts to specific line items by MPN and Adobe Order ID +- Handles multiple discounts per item (concatenated with commas) +- Shows "-" when no discounts are applied + +### 2. Discount Group Information +Provides both formatted and raw discount group data: +- **Formatted** (Column 11): Human-readable levels (e.g., "Level 1", "TLP Level 2") +- **Raw Licenses** (Column 12): Unformatted code for licenses (e.g., "01A12") +- **Raw Consumables** (Column 13): Unformatted code for consumables (e.g., "T1A12") + +### 3. Unit of Measure +Indicates how each product is licensed or billed: +- **User** - Per-user licenses (Creative Cloud, Acrobat, etc.) +- **Transactions** - Transaction-based (Adobe Sign) +- **Per Server** - Server-based licensing +- **Units** - Generic unit measurement +- **Credits** - Credit-based consumption (Adobe Stock) + +### 4. Prorata Calculation +Automatically calculates the number of days between the effective date and renewal date: +- Useful for mid-cycle purchases and changes +- Helps with proration calculations +- Returns "-" if dates are missing + +### 5. Commitment Tracking +Tracks Adobe commitment terms: +- Initial commitment status and dates +- Recommitment status and dates +- Supports multi-year agreements + +### 6. Assignee Information +Tracks request ownership: +- Assignee ID and name +- Helps with workflow management and accountability + +--- + +## Use Cases + +### Financial Analysis +- Track costs, reseller pricing, and MSRP across products +- Analyze discount effectiveness (flex discounts + volume discounts) +- Calculate proration for mid-cycle changes +- Monitor pricing levels and discount groups + +### License Management +- Track seat counts and delta changes +- Monitor license types (User, Transaction, etc.) +- Identify growth trends by product +- Manage license optimization + +### Compliance & Auditing +- Verify Adobe order numbers and VIP information +- Track commitment periods and terms +- Audit discount applications +- Review request assignments and approval chains + +### Partner Management +- Analyze reseller/distributor activity +- Track customer portfolio by partner +- Monitor marketplace distribution +- Review partner pricing and margins + +### Product Mix Analysis +- Identify most popular products and SKUs +- Track product periods (monthly vs yearly) +- Analyze unit of measure distribution +- Monitor product adoption trends + +--- + +## Execution Commands + +### Using Connect CLI + +```bash +# Execute report with default parameters +ccli report execute requests -d . + +# Execute with specific date range +ccli report execute requests \ + --param date_from=2024-01-01 \ + --param date_to=2024-12-31 \ + -d . + +# Execute for specific product +ccli report execute requests \ + --param product=PRD-123-456-789 \ + -d . + +# Execute for specific marketplace +ccli report execute requests \ + --param marketplace=MP-12345 \ + -d . + +# Execute for specific request types +ccli report execute requests \ + --param rr_type=purchase,change \ + -d . +``` + +### Using Docker Environment + +```bash +# Navigate to the project directory +cd /home/connect/adobe_reports + +# Execute the report +ccli report execute requests -d . +``` + +--- + +## Data Quality Notes + +### Expected Values +- All columns should populate for complete requests +- "-" indicates missing or not applicable data +- Flex discount columns show "-" when no discounts are applied +- Commitment fields show "-" when no commitment exists + +### Common Scenarios +- **New Purchases**: Order Delta shows positive numbers +- **Cancellations**: Order Delta shows negative numbers +- **Changes**: May show both positive and negative deltas +- **Flex Discounts**: Only populated when discounts are applied to specific items +- **Commitments**: Only populated for customers with commitment agreements + +--- + +## Technical Details + +### Data Sources +- **Request Data**: CloudBlue Connect Requests API +- **Asset Data**: CloudBlue Connect Assets API +- **Financial Data**: CloudBlue Connect Products/Pricelist API +- **Parameters**: Asset fulfillment and configuration parameters + +### Special Processing +1. **Flex Discounts**: Parsed from `cb_flex_discounts_applied` object parameter +2. **Prorata**: Calculated from effective date and renewal date +3. **Pricing Level**: Transformed from raw discount group code +4. **Unit of Measure**: Extracted from item type field +5. **Dates**: Normalized and formatted consistently + +### Performance Considerations +- Large date ranges may take longer to process +- Report includes API calls per request and asset +- Filtering by product/marketplace improves performance +- Consider running reports for specific periods (monthly/quarterly) + +--- + +## Related Documentation + +- **IMPLEMENTATION_COMPLETE.md** - Complete implementation history and technical details +- **FLEX_DISCOUNTS_IMPLEMENTATION.md** - Detailed flex discounts implementation +- **PRORATA_FIX_FINAL.md** - Prorata calculation implementation and fixes +- **DISCOUNT_GROUP_COLUMN_ADDED.md** - Discount group licenses column details +- **DISCOUNT_GROUP_CONSUMABLES_COLUMN.md** - Discount group consumables column details +- **UNIT_OF_MEASURE_COLUMN.md** - Unit of measure column implementation +- **PRICING_SKU_LEVEL_EXPLANATION.md** - Pricing level calculation logic + +--- + +## Version History + +**Current Version**: 1.4 +**Last Updated**: November 2025 +**Total Columns**: 53 +**Status**: Production-ready + +**Recent Changes**: +- ✅ Added Adobe Flex Discounts support (4 columns) +- ✅ Restored missing columns from v1.3.1 (11 columns) +- ✅ Added Discount Group Licenses column (raw value) +- ✅ Added Discount Group Consumables column (raw value) +- ✅ Added Unit of Measure column (item type) +- ✅ Fixed Prorata calculation for various date formats +- ✅ Updated test coverage for all new features + +--- + +## Support & Troubleshooting + +### Common Issues + +**Missing Flex Discount Data** +- Verify `cb_flex_discounts_applied` parameter exists on asset +- Check that MPN and Adobe Order ID match between discount and line item +- Confirm discount data structure is correct (JSON with 'discounts' array) + +**Prorata Shows "-"** +- Verify effective date and renewal date are populated +- Check date formats are valid ISO 8601 or YYYY-MM-DD +- Ensure dates are not in the future + +**Discount Group Shows "-"** +- Verify `discount_group` or `discount_group_consumables` parameter exists +- Check parameter is at the asset level (fulfillment parameters) +- Confirm parameter value is not empty + +**Unit of Measure Shows "-"** +- Verify item has a `type` field in the request data +- Check item structure is complete +- May indicate incomplete data sync from Adobe + +### Getting Help +For technical support or questions about this report: +1. Check the related documentation files +2. Review test cases in `tests/test_request_report.py` +3. Examine sample data in `tests/ff_requests.json` +4. Contact CloudBlue Connect support + +--- + +**Report ID**: `requests` +**Report Name**: Adobe Approved Requests +**Maintained by**: CloudBlue Adobe Reports Team \ No newline at end of file diff --git a/reports/requests/entrypoint.py b/reports/requests/entrypoint.py index cbb48e1..e6b0e2a 100644 --- a/reports/requests/entrypoint.py +++ b/reports/requests/entrypoint.py @@ -22,7 +22,19 @@ def generate(client, parameters, progress_callback, renderer_type=None, extra_co action = utils.get_param_value(parameters_list, 'action_type') adobe_user_email = utils.get_param_value(parameters_list, 'adobe_user_email') adobe_cloud_program_id = utils.get_param_value(parameters_list, 'adobe_customer_id') - pricing_level = utils.get_discount_level(utils.get_param_value(parameters_list, 'discount_group')) + discount_group = utils.get_param_value(parameters_list, 'discount_group') + discount_group_consumables = utils.get_param_value(parameters_list, 'discount_group_consumables') + pricing_level = utils.get_discount_level(discount_group) + commitment = utils.get_param_value(parameters_list, 'commitment_status') + commitment_start_date = utils.get_param_value(parameters_list, 'commitment_start_date') + commitment_end_date = utils.get_param_value(parameters_list, 'commitment_end_date') + recommitment = utils.get_param_value(parameters_list, 'recommitment_status') + recommitment_start_date = utils.get_param_value(parameters_list, 'recommitment_start_date') + recommitment_end_date = utils.get_param_value(parameters_list, 'recommitment_end_date') + external_reference_id = utils.get_param_value(parameters_list, 'external_reference_id') + renewal_date = utils.get_param_value(parameters_list, 'renewal_date') + effective_date = utils.get_basic_value(request, 'effective_date') + prorata_days = utils.get_days_between_effective_and_renewal_date(effective_date, renewal_date) # get currency from configuration params currency = utils.get_param_value(request['asset']['configuration']['params'], 'Adobe_Currency') @@ -35,21 +47,36 @@ def generate(client, parameters, progress_callback, renderer_type=None, extra_co delta_str = _get_delta_str(item) if delta_str == '': continue + + # Get flex discounts for this item + item_mpn = utils.get_basic_value(item, 'mpn') + item_type = utils.get_basic_value(item, 'type') + flex_discounts = utils.get_flex_discounts(parameters_list, item_mpn, order_number) + yield ( utils.get_basic_value(request, 'id'), # Request ID + utils.get_value(request, 'assignee', 'id'), # Assignee ID + utils.get_value(request, 'assignee', 'name'), # Assignee Name utils.get_value(request, 'asset', 'id'), # Connect Subscription ID utils.get_value(request, 'asset', 'external_id'), # End Customer Subscription ID - action, # Type of Purchase + action, # Action order_number, # Adobe Order # transfer_number, # Adobe Transfer ID # vip_number, # VIP # adobe_cloud_program_id, # Adobe Cloud Program ID pricing_level, # Pricing SKU Level (Volume Discount level) + discount_group, # Discount Group Licenses + discount_group_consumables, # Discount Group Consumables utils.get_basic_value(item, 'display_name'), # Product Description - utils.get_basic_value(item, 'mpn'), # Part Number + item_mpn, # Part Number + item_type, # Unit of Measure utils.get_basic_value(item, 'period'), # Product Period utils.get_basic_value(item, 'quantity'), # Cumulative Seat delta_str, # Order Delta + flex_discounts['discounted_mpn'], # Discounted MPN + flex_discounts['discounted_order_id'], # Discounted Adobe Order Id + flex_discounts['discount_id'], # Adobe Discount Id + flex_discounts['discount_code'], # Adobe Discount Code utils.get_value(request['asset']['tiers'], 'tier1', 'id'), # Reseller ID utils.get_value(request['asset']['tiers'], 'tier1', 'name'), # Reseller Name utils.get_value(request['asset']['tiers'], 'customer', 'name'), # End Customer Name @@ -60,21 +87,32 @@ def generate(client, parameters, progress_callback, renderer_type=None, extra_co utils.get_value(request['asset'], 'product', 'id'), # Product ID utils.get_value(request['asset'], 'product', 'name'), # Product Name utils.get_value(request, 'asset', 'status'), # Subscription Status - utils.get_value(subscription, 'billing', 'next_date'), # Anniversary Date utils.convert_to_datetime( - utils.get_basic_value(request, 'effective_date'), # Effective Date + utils.get_value(subscription, 'billing', 'next_date'), # Anniversary Date ), + renewal_date, # Adobe Renewal Date utils.convert_to_datetime( - utils.get_basic_value(request, 'created'), # Creation Date + effective_date, # Effective Date ), - utils.get_basic_value(request, 'type'), # Transaction Type + prorata_days, # Prorata (days) + utils.convert_to_datetime( + utils.get_basic_value(request, 'created'), # Creation Date + ), + utils.get_basic_value(request, 'type'), # Connect Order Type adobe_user_email, # Adobe User Email currency, # Currency utils.get_value(financials, item['global_id'], 'cost'), # Cost utils.get_value(financials, item['global_id'], 'reseller_cost'), # Reseller Cost utils.get_value(financials, item['global_id'], 'msrp'), # MSRP - utils.get_basic_value(request['asset']['connection'], 'type'), # Connection Type, + utils.get_basic_value(request['asset']['connection'], 'type'), # Connection Type utils.today_str(), # Exported At + commitment, # commitment + commitment_start_date, # commitment start date + commitment_end_date, # commitment end date + recommitment, # recommitment + recommitment_start_date, # recommitment start date + recommitment_end_date, # recommitment end date + external_reference_id, # external reference id ) progress += 1 progress_callback(progress, total) diff --git a/reports/requests/templates/xlsx/template.xlsx b/reports/requests/templates/xlsx/template.xlsx index fc327f3a5cf2322c56c0cff062e311f4211fb686..e9c98ab28af5657cd0c58c16d92a1fb7b4e28c26 100644 GIT binary patch delta 6267 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 diff --git a/reports/utils.py b/reports/utils.py index 9a12c9e..d48526f 100644 --- a/reports/utils.py +++ b/reports/utils.py @@ -1,4 +1,5 @@ import datetime +import json from reports import api_calls BASE_CURRENCY = 'USD' @@ -242,7 +243,119 @@ def get_base_currency_financials(financials_and_seats: dict, currency: dict) -> def get_financials_from_product_per_marketplace(client, marketplace_id, asset_id): listing = api_calls.request_listing(client, marketplace_id, asset_id) price_list_points = [] - if listing and listing['pricelist']: + if listing and listing.get('pricelist'): price_list_version = api_calls.request_price_list(client, listing['pricelist']['id']) - price_list_points = api_calls.request_price_list_version_points(client, price_list_version['id']) + if price_list_version: + price_list_points = api_calls.request_price_list_version_points(client, price_list_version['id']) return get_financials_from_price_list(price_list_points) + + +def get_days_between_effective_and_renewal_date(effective_date, renewal_date): + """ + Calculate the number of days between effective date and renewal date. + + :type effective_date: str + :type renewal_date: str + :param effective_date: Effective date in ISO format (e.g., "2020-11-23T12:52:27+00:00") + :param renewal_date: Renewal date in YYYY-MM-DD format (e.g., "2020-12-01") + :return: Number of days between the two dates, or '-' if calculation fails + """ + try: + # Handle empty or missing values + if not effective_date or effective_date == '-' or not renewal_date or renewal_date == '-': + return "-" + + # Normalize the effective_date string (same approach as convert_to_datetime) + # Remove timezone info and convert T to space for consistent parsing + normalized_effective = effective_date.replace("T", " ").replace("+00:00", "").strip() + + # Parse the normalized effective date + effective = datetime.datetime.strptime(normalized_effective, "%Y-%m-%d %H:%M:%S") + effective_ymd = datetime.datetime(effective.year, effective.month, effective.day) + + # Parse renewal date + renewal = datetime.datetime.strptime(renewal_date, "%Y-%m-%d") + + return (renewal - effective_ymd).days + except Exception: + return "-" + + +def get_flex_discounts(params: list, item_mpn: str, order_id: str) -> dict: + """ + Parse the cb_flex_discounts_applied parameter and match discounts for a specific item. + + :type params: list + :type item_mpn: str + :type order_id: str + :param params: asset parameters list + :param item_mpn: MPN of the item to match + :param order_id: Adobe Order ID to match + :return: dict with matched discount fields or '-' if not found + """ + result = { + 'discounted_mpn': '-', + 'discounted_order_id': '-', + 'discount_id': '-', + 'discount_code': '-', + } + + try: + # Find the cb_flex_discounts_applied parameter + flex_param = None + for param in params: + if param.get('id') == 'cb_flex_discounts_applied': + flex_param = param + break + + if not flex_param: + return result + + # For object-type parameters, data is in 'structured_value', not 'value' + flex_discounts_data = None + if 'structured_value' in flex_param and flex_param['structured_value']: + flex_discounts_data = flex_param['structured_value'] + elif 'value' in flex_param and flex_param['value']: + # Fallback: try parsing from 'value' field if it's a JSON string + value = flex_param['value'] + if value and value != '-': + flex_discounts_data = json.loads(value) + + if not flex_discounts_data: + return result + + # Get discounts array + discounts = flex_discounts_data.get('discounts', []) + + if not discounts: + return result + + # Find matching discounts for this item + matched_mpns = [] + matched_order_ids = [] + matched_discount_ids = [] + matched_discount_codes = [] + + for discount in discounts: + discount_mpn = discount.get('mpn', '') + discount_order_id = discount.get('order_id', '') + + # Match by MPN and Order ID + if discount_mpn == item_mpn and discount_order_id == order_id: + matched_mpns.append(discount_mpn) + matched_order_ids.append(discount_order_id) + matched_discount_ids.append(discount.get('id', '')) + matched_discount_codes.append(discount.get('code', '')) + + # If matches found, concatenate with comma + if matched_mpns: + result['discounted_mpn'] = ','.join(matched_mpns) + result['discounted_order_id'] = ','.join(matched_order_ids) + result['discount_id'] = ','.join(matched_discount_ids) + result['discount_code'] = ','.join(matched_discount_codes) + + except (json.JSONDecodeError, TypeError, KeyError, AttributeError): + # If any error occurs (invalid JSON, wrong structure, etc.), return default values + pass + + return result diff --git a/tests/ff_requests.json b/tests/ff_requests.json index b7b7aa4..10d2cd4 100644 --- a/tests/ff_requests.json +++ b/tests/ff_requests.json @@ -505,6 +505,25 @@ "readonly": false } }, + { + "id": "discount_group_consumables", + "name": "discount_group_consumables", + "type": "text", + "phase": "fulfillment", + "description": "discount group consumables", + "value": "T1A12", + "value_error": "", + "title": "discount group consumables", + "constraints": { + "placeholder": "discount group consumables", + "hint": "discount group consumables", + "meta": {}, + "required": false, + "reconciliation": false, + "hidden": false, + "readonly": false + } + }, { "id": "t0_f_password", "name": "t0_f_password", @@ -859,6 +878,181 @@ "meta": {}, "required": false } + }, + { + "id": "cb_flex_discounts_applied", + "name": "cb_flex_discounts_applied", + "type": "object", + "phase": "fulfillment", + "description": "Adobe Flex Discounts Applied", + "value": "", + "value_error": "", + "title": "Adobe Flex Discounts Applied", + "constraints": { + "meta": {}, + "reconciliation": false, + "required": false, + "hidden": true, + "readonly": true + }, + "structured_value": { + "discounts": [ + { + "mpn": "MPN-R-002", + "order_id": "P9201150234", + "id": "12345678-1234-1234-1234-123456789abc", + "code": "ADOBE_PROMOTION_Q1" + }, + { + "mpn": "MPN-R-005", + "order_id": "P9201150234", + "id": "87654321-4321-4321-4321-cba987654321", + "code": "ADOBE_DISCOUNT_SPECIAL" + }, + { + "mpn": "MPN-R-005", + "order_id": "P9201150234", + "id": "11111111-2222-3333-4444-555555555555", + "code": "ADOBE_EXTRA_SAVINGS" + } + ] + } + }, + { + "id": "commitment_status", + "name": "commitment_status", + "type": "text", + "phase": "fulfillment", + "description": "Commitment Status", + "value": "COMMITTED", + "value_error": "", + "title": "Commitment Status", + "constraints": { + "meta": {}, + "reconciliation": false, + "required": false, + "hidden": false, + "readonly": false + } + }, + { + "id": "commitment_start_date", + "name": "commitment_start_date", + "type": "text", + "phase": "fulfillment", + "description": "Commitment Start Date", + "value": "2023-11-23", + "value_error": "", + "title": "Commitment Start Date", + "constraints": { + "meta": {}, + "reconciliation": false, + "required": false, + "hidden": false, + "readonly": false + } + }, + { + "id": "commitment_end_date", + "name": "commitment_end_date", + "type": "text", + "phase": "fulfillment", + "description": "Commitment End Date", + "value": "2026-11-23", + "value_error": "", + "title": "Commitment End Date", + "constraints": { + "meta": {}, + "reconciliation": false, + "required": false, + "hidden": false, + "readonly": false + } + }, + { + "id": "recommitment_status", + "name": "recommitment_status", + "type": "text", + "phase": "fulfillment", + "description": "Recommitment Status", + "value": "-", + "value_error": "", + "title": "Recommitment Status", + "constraints": { + "meta": {}, + "reconciliation": false, + "required": false, + "hidden": false, + "readonly": false + } + }, + { + "id": "recommitment_start_date", + "name": "recommitment_start_date", + "type": "text", + "phase": "fulfillment", + "description": "Recommitment Start Date", + "value": "-", + "value_error": "", + "title": "Recommitment Start Date", + "constraints": { + "meta": {}, + "reconciliation": false, + "required": false, + "hidden": false, + "readonly": false + } + }, + { + "id": "recommitment_end_date", + "name": "recommitment_end_date", + "type": "text", + "phase": "fulfillment", + "description": "Recommitment End Date", + "value": "-", + "value_error": "", + "title": "Recommitment End Date", + "constraints": { + "meta": {}, + "reconciliation": false, + "required": false, + "hidden": false, + "readonly": false + } + }, + { + "id": "external_reference_id", + "name": "external_reference_id", + "type": "text", + "phase": "fulfillment", + "description": "External Reference ID", + "value": "EXT-REF-12345", + "value_error": "", + "title": "External Reference ID", + "constraints": { + "meta": {}, + "reconciliation": false, + "required": false, + "hidden": false, + "readonly": false + } + }, + { + "id": "renewal_date", + "name": "renewal_date", + "type": "text", + "phase": "fulfillment", + "description": "Renewal Date", + "value": "2021-11-23", + "value_error": "", + "title": "Renewal Date", + "constraints": { + "meta": {}, + "reconciliation": false, + "required": false, + "hidden": false, + "readonly": false + } } ], "tiers": { diff --git a/tests/test_request_report.py b/tests/test_request_report.py index 847ba55..44a0516 100644 --- a/tests/test_request_report.py +++ b/tests/test_request_report.py @@ -69,5 +69,56 @@ def test_requests_generate(sync_client_factory, response_factory, progress, ] client = sync_client_factory(responses) - result = entrypoint.generate(client, parameters, progress) - assert len(list(result)) == 6 # number of items on ff_request.json + result = list(entrypoint.generate(client, parameters, progress)) + + # Verify number of rows (items with non-empty delta) + assert len(result) == 6 # number of items on ff_request.json + + # Verify each row has 53 columns (46 original + 4 flex discount + 2 discount group + 1 UoM) + for row in result: + assert len(row) == 53 + + # Verify basic data in first row + first_row = result[0] + assert first_row[0] == 'PR-1895-0864-1238-001' # Request ID + assert first_row[1] == 'UR-841-574-187' # Assignee ID + assert first_row[2] == 'Marc Serrat' # Assignee Name + assert first_row[3] == 'AS-1895-0864-1238' # Connect Subscription ID + assert first_row[11] == '01A12' # Discount Group Licenses (position 12) + assert first_row[12] == 'T1A12' # Discount Group Consumables (position 13) + assert first_row[14] == 'MPN-R-001' # Part Number (position 15) + assert first_row[15] == 'Units' # Unit of Measure (position 16) + assert first_row[16] == 'Monthly' # Product Period (position 17, shifted by +1) + + # Verify discount data for specific items + # Item with MPN-R-002 should have one discount + item_002 = [row for row in result if row[14] == 'MPN-R-002'][0] + assert item_002[19] == 'MPN-R-002' # Discounted MPN (shifted from 18 to 19) + assert item_002[20] == 'P9201150234' # Discounted Adobe Order Id (shifted from 19 to 20) + assert item_002[21] == '12345678-1234-1234-1234-123456789abc' # Adobe Discount Id (shifted from 20 to 21) + assert item_002[22] == 'ADOBE_PROMOTION_Q1' # Adobe Discount Code (shifted from 21 to 22) + + # Item with MPN-R-005 should have two discounts (concatenated) + item_005 = [row for row in result if row[14] == 'MPN-R-005'][0] + assert item_005[19] == 'MPN-R-005,MPN-R-005' # Discounted MPN (shifted from 18 to 19) + assert item_005[20] == 'P9201150234,P9201150234' # Discounted Adobe Order Id (shifted from 19 to 20) + assert item_005[21] == '87654321-4321-4321-4321-cba987654321,11111111-2222-3333-4444-555555555555' + assert item_005[22] == 'ADOBE_DISCOUNT_SPECIAL,ADOBE_EXTRA_SAVINGS' + + # Item with MPN-R-001 should have no discount (show '-') + item_001 = [row for row in result if row[14] == 'MPN-R-001'][0] + assert item_001[19] == '-' # Discounted MPN (shifted from 18 to 19) + assert item_001[20] == '-' # Discounted Adobe Order Id (shifted from 19 to 20) + assert item_001[21] == '-' # Adobe Discount Id (shifted from 20 to 21) + assert item_001[22] == '-' # Adobe Discount Code (shifted from 21 to 22) + + # Verify new columns (commitment, renewal date, etc.) + assert item_001[34] == '2021-11-23' # Adobe Renewal Date (shifted from 33 to 34) + assert item_001[36] == 365 # Prorata (days) (shifted from 35 to 36) + assert item_001[46] == 'COMMITTED' # commitment (shifted from 45 to 46) + assert item_001[47] == '2023-11-23' # commitment start date (shifted from 46 to 47) + assert item_001[48] == '2026-11-23' # commitment end date (shifted from 47 to 48) + assert item_001[49] == '-' # recommitment (shifted from 48 to 49) + assert item_001[50] == '-' # recommitment start date (shifted from 49 to 50) + assert item_001[51] == '-' # recommitment end date (shifted from 50 to 51) + assert item_001[52] == 'EXT-REF-12345' # external reference id (shifted from 51 to 52) diff --git a/tests/test_utils.py b/tests/test_utils.py index ac3243d..91c1b58 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -68,3 +68,205 @@ def test_discount_level(): assert utils.get_discount_level(group1) == 'Level 1' assert utils.get_discount_level(group2) == 'TLP Level 1' assert utils.get_discount_level('nothing') == 'Empty' + + +def test_get_flex_discounts_with_single_match(): + """Test get_flex_discounts with a single matching discount using structured_value""" + params = [ + { + 'id': 'cb_flex_discounts_applied', + 'type': 'object', + 'value': '', + 'structured_value': { + 'discounts': [ + { + 'mpn': '65304520CA', + 'order_id': 'P9201911604', + 'id': '55555555-8768-4e8a-9a2f-fb6a6b08f557', + 'code': 'ADOBE_ALL_PROMOTION' + } + ] + } + } + ] + result = utils.get_flex_discounts(params, '65304520CA', 'P9201911604') + assert result['discounted_mpn'] == '65304520CA' + assert result['discounted_order_id'] == 'P9201911604' + assert result['discount_id'] == '55555555-8768-4e8a-9a2f-fb6a6b08f557' + assert result['discount_code'] == 'ADOBE_ALL_PROMOTION' + + +def test_get_flex_discounts_with_multiple_matches(): + """Test get_flex_discounts with multiple matching discounts (concatenated)""" + params = [ + { + 'id': 'cb_flex_discounts_applied', + 'type': 'object', + 'value': '', + 'structured_value': { + 'discounts': [ + { + 'mpn': '65304520CA', + 'order_id': 'P9201911604', + 'id': '11111111-1111-1111-1111-111111111111', + 'code': 'PROMO_1' + }, + { + 'mpn': '65304520CA', + 'order_id': 'P9201911604', + 'id': '22222222-2222-2222-2222-222222222222', + 'code': 'PROMO_2' + } + ] + } + } + ] + result = utils.get_flex_discounts(params, '65304520CA', 'P9201911604') + assert result['discounted_mpn'] == '65304520CA,65304520CA' + assert result['discounted_order_id'] == 'P9201911604,P9201911604' + assert result['discount_id'] == '11111111-1111-1111-1111-111111111111,22222222-2222-2222-2222-222222222222' + assert result['discount_code'] == 'PROMO_1,PROMO_2' + + +def test_get_flex_discounts_no_match(): + """Test get_flex_discounts when no matching discount is found""" + params = [ + { + 'id': 'cb_flex_discounts_applied', + 'type': 'object', + 'value': '', + 'structured_value': { + 'discounts': [ + { + 'mpn': '65304520CA', + 'order_id': 'P9201911604', + 'id': '55555555-8768-4e8a-9a2f-fb6a6b08f557', + 'code': 'ADOBE_ALL_PROMOTION' + } + ] + } + } + ] + result = utils.get_flex_discounts(params, 'DIFFERENT_MPN', 'P9201911604') + assert result['discounted_mpn'] == '-' + assert result['discounted_order_id'] == '-' + assert result['discount_id'] == '-' + assert result['discount_code'] == '-' + + +def test_get_flex_discounts_missing_parameter(): + """Test get_flex_discounts when parameter doesn't exist""" + params = [ + { + 'id': 'some_other_param', + 'value': 'some_value', + } + ] + result = utils.get_flex_discounts(params, '65304520CA', 'P9201911604') + assert result['discounted_mpn'] == '-' + assert result['discounted_order_id'] == '-' + assert result['discount_id'] == '-' + assert result['discount_code'] == '-' + + +def test_get_flex_discounts_with_json_string(): + """Test get_flex_discounts with JSON string in value field (backward compatibility)""" + params = [ + { + 'id': 'cb_flex_discounts_applied', + 'value': '{"discounts":[{"mpn":"65304520CA","order_id":"P9201911604","id":"55555555-8768-4e8a-9a2f-fb6a6b08f557","code":"ADOBE_ALL_PROMOTION"}]}', + } + ] + result = utils.get_flex_discounts(params, '65304520CA', 'P9201911604') + assert result['discounted_mpn'] == '65304520CA' + assert result['discounted_order_id'] == 'P9201911604' + assert result['discount_id'] == '55555555-8768-4e8a-9a2f-fb6a6b08f557' + assert result['discount_code'] == 'ADOBE_ALL_PROMOTION' + + +def test_get_flex_discounts_invalid_json(): + """Test get_flex_discounts with invalid JSON""" + params = [ + { + 'id': 'cb_flex_discounts_applied', + 'value': 'invalid json {{{', + } + ] + result = utils.get_flex_discounts(params, '65304520CA', 'P9201911604') + assert result['discounted_mpn'] == '-' + assert result['discounted_order_id'] == '-' + assert result['discount_id'] == '-' + assert result['discount_code'] == '-' + + +def test_get_days_between_effective_and_renewal_date_with_timezone(): + """Test prorata calculation with timezone in effective date""" + result = utils.get_days_between_effective_and_renewal_date( + '2020-11-23T12:52:27+00:00', + '2021-11-23' + ) + assert result == 365 + + +def test_get_days_between_effective_and_renewal_date_without_timezone(): + """Test prorata calculation without timezone in effective date""" + result = utils.get_days_between_effective_and_renewal_date( + '2020-11-23T12:52:27', + '2021-11-23' + ) + assert result == 365 + + +def test_get_days_between_effective_and_renewal_date_space_separator(): + """Test prorata calculation with space separator in effective date""" + result = utils.get_days_between_effective_and_renewal_date( + '2020-11-23 12:52:27', + '2021-11-23' + ) + assert result == 365 + + +def test_get_days_between_effective_and_renewal_date_real_data(): + """Test prorata calculation with real data from report""" + result = utils.get_days_between_effective_and_renewal_date( + '2025-10-10T03:20:04+00:00', + '2026-09-08' + ) + assert result == 333 + + +def test_get_days_between_effective_and_renewal_date_missing_effective(): + """Test prorata calculation with missing effective date""" + result = utils.get_days_between_effective_and_renewal_date( + '-', + '2021-11-23' + ) + assert result == '-' + + +def test_get_days_between_effective_and_renewal_date_missing_renewal(): + """Test prorata calculation with missing renewal date""" + result = utils.get_days_between_effective_and_renewal_date( + '2020-11-23T12:52:27+00:00', + '-' + ) + assert result == '-' + + +def test_get_flex_discounts_empty_discounts(): + """Test get_flex_discounts with empty discounts array""" + params = [ + { + 'id': 'cb_flex_discounts_applied', + 'type': 'object', + 'value': '', + 'structured_value': { + 'discounts': [] + } + } + ] + result = utils.get_flex_discounts(params, '65304520CA', 'P9201911604') + assert result['discounted_mpn'] == '-' + assert result['discounted_order_id'] == '-' + assert result['discount_id'] == '-' + assert result['discount_code'] == '-'