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 fc327f3..e9c98ab 100644 Binary files a/reports/requests/templates/xlsx/template.xlsx and b/reports/requests/templates/xlsx/template.xlsx differ 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'] == '-'