Files
ewoooc/tests/test_sales_chart_data_contract.py

155 lines
5.3 KiB
Python

import pandas as pd
from routes.sales_routes import _build_top_product_chart_data
from services.sales_analysis_chart_service import (
build_analysis_scope,
build_bcg_chart_data,
build_heatmap_chart_data,
build_marketing_chart_data,
build_sales_filter_options,
build_seasonality_chart_data,
build_treemap_chart_data,
resolve_yoy_year_options,
)
def test_top_product_chart_aggregates_sku_rows_before_ranking():
rows = pd.DataFrame(
[
{'pid': 'P1', 'name': '同名商品', 'amount': 100},
{'pid': 'P1', 'name': '同名商品', 'amount': 50},
{'pid': 'P2', 'name': '同名商品', 'amount': 90},
{'pid': 'P3', 'name': '獨立商品', 'amount': 200},
{'pid': 'P4', 'name': '零業績商品', 'amount': 0},
]
)
result = _build_top_product_chart_data(rows, 'pid', 'name', 'amount', 'amount')
assert result['chart_values'] == [200.0, 150.0, 90.0]
assert result['labels'] == ['獨立商品', '同名商品 [P1]', '同名商品 [P2]']
assert result['metric_label'] == '銷售金額 ($)'
def test_top_product_chart_returns_explicit_empty_contract():
result = _build_top_product_chart_data(
pd.DataFrame(columns=['name', 'amount']),
None,
'name',
'amount',
'qty',
)
assert result == {
'labels': [],
'chart_values': [],
'metric_label': '銷售數量',
'is_money': False,
}
def test_sales_chart_builders_emit_the_contract_consumed_by_frontend():
frame = pd.DataFrame([
{
'pid': 'P1', 'name': '商品 A', 'category': '美妝', 'amount': 100,
'qty': 2, 'calculated_profit': 30, '_month_str': '2026-04',
'_dow': 2, '_hour': 10,
},
{
'pid': 'P1', 'name': '商品 A', 'category': '美妝', 'amount': 50,
'qty': 1, 'calculated_profit': 10, '_month_str': '2026-04',
'_dow': 2, '_hour': 10,
},
{
'pid': 'P2', 'name': '商品 B', 'category': '3C', 'amount': 300,
'qty': 3, 'calculated_profit': 60, '_month_str': '2026-05',
'_dow': 4, '_hour': 21,
},
])
treemap = build_treemap_chart_data(frame, 'category', 'name', 'amount')
bcg = build_bcg_chart_data(frame, 'pid', 'name', 'qty', 'amount')
heatmap = build_heatmap_chart_data(frame, 'amount')
seasonality = build_seasonality_chart_data(frame, 'category', 'amount')
assert treemap[0].keys() == {'category', 'name', 'value'}
assert {item['name'] for item in treemap} == {'商品 A', '商品 B'}
assert bcg['points'][0]['name'] == '商品 B'
assert bcg['points'][1]['x'] == 3
assert bcg['x_median'] == 3
assert len(heatmap['matrix']) == 7
assert all(len(row) == 24 for row in heatmap['matrix'])
assert heatmap['matrix'][2][10] == 150
assert heatmap['matrix'][4][21] == 300
assert seasonality == {
'categories': ['3C', '美妝'],
'months': ['2026-04', '2026-05'],
'matrix': [[0.0, 300.0], [150.0, 0.0]],
}
def test_sales_chart_builders_return_explicit_empty_contracts():
empty = pd.DataFrame()
assert build_treemap_chart_data(empty, 'category', 'name', 'amount') == []
assert build_bcg_chart_data(empty, 'pid', 'name', 'qty', 'amount') == {
'points': [], 'x_median': 0.0, 'y_median': 0.0,
}
assert build_heatmap_chart_data(empty, 'amount')['matrix'] == []
assert build_seasonality_chart_data(empty, 'category', 'amount')['matrix'] == []
def test_marketing_and_yoy_controls_follow_active_metric_and_period():
marketing = build_marketing_chart_data({
'discount': [{'name': '母親節', 'revenue': 500, 'qty': 5, 'profit': 120}],
'coupon': [{'name': '折價券', 'revenue': 300, 'qty': 3, 'profit': 80}],
}, 'profit')
years = resolve_yoy_year_options(
['2024-12', '2026-04', float('nan')],
{'end_date': '2026-04-30'},
)
assert marketing['metric'] == 'profit'
assert marketing['discount'] == {'labels': ['母親節'], 'values': [120.0]}
assert marketing['coupon'] == {'labels': ['折價券'], 'values': [80.0]}
assert years == {'years': [2026, 2025, 2024], 'year1': 2025, 'year2': 2026}
def test_filter_options_are_scoped_to_the_loaded_period_frame():
frame = pd.DataFrame([
{'category': '美妝', 'brand': 'A', '_month_str': '2026-04'},
{'category': '3C', 'brand': 'B', '_month_str': '2026-04'},
{'category': '美妝', 'brand': 'A', '_month_str': None},
])
options = build_sales_filter_options(frame, {
'category': 'category',
'brand': 'brand',
'vendor': None,
'activity': None,
'payment': None,
})
assert options['categories'] == ['3C', '美妝']
assert options['brands'] == ['A', 'B']
assert options['months'] == ['2026-04']
def test_analysis_scope_names_every_active_time_filter():
scope = build_analysis_scope(
{
'label': '最近 6 個月',
'start_date': '2026-01-23',
'end_date': '2026-07-22',
},
month='2026-04',
dow='2',
hour='9',
)
assert scope == {
'label': '最近 6 個月 · 月份 2026-04 · 週三 · 09:00',
'start_date': '2026-01-23',
'end_date': '2026-07-22',
}