Metadata-Version: 2.4
Name: bg4h-discount-api
Version: 0.1.0
Summary: Motor Python reutilizable para calcular descuentos, promociones y excepciones de BaseGes.
Author-email: "ct.galega" <soporte@ctgalega.com>
License-Expression: MIT
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Operating System :: OS Independent
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: bg4h>=1.0.70
Requires-Dist: pypika<0.52,>=0.48.9
Provides-Extra: mssql
Requires-Dist: pymssql<3,>=2.3; extra == "mssql"
Provides-Extra: test
Requires-Dist: pytest<9,>=8; extra == "test"
Requires-Dist: pytest-cov<8,>=5; extra == "test"
Provides-Extra: dev
Requires-Dist: pytest<9,>=8; extra == "dev"
Requires-Dist: pytest-cov<8,>=5; extra == "dev"
Requires-Dist: ruff>=0.4; extra == "dev"
Requires-Dist: mypy>=1.10; extra == "dev"
Requires-Dist: build>=1.2; extra == "dev"
Requires-Dist: twine>=5; extra == "dev"
Dynamic: license-file

# bg4h-discount-api

Typed Python pricing engine for BaseGes articles. It resolves article discounts, customer discount tables, promotions, and customer exceptions while preserving BaseGes priority rules.

> **Production-ready** — independent of Frappe and suitable for reuse by multiple applications and servers.

## Installation

```bash
python -m pip install bg4h-discount-api
python -m pip install "bg4h-discount-api[mssql]"
python -m pip install -e ".[dev,mssql]"
```

`bg4h>=1.0.70` is installed automatically as a required dependency.

## Features

- ✅ Typed pricing models based on dataclasses
- ✅ Single-article and true batch calculations
- ✅ BaseGes SQL Server repository included
- ✅ In-memory reuse of customer and rule data during each operation
- ✅ Article, family, subfamily, group, key, customer, and customer-type rules
- ✅ Quantity and minimum-amount eligibility
- ✅ Deterministic priority resolution and BaseGes tie-breaking
- ✅ JSON-ready result serialization
- ✅ No dependency on Frappe or a web framework

## Public API

| Class or method | Description |
|---|---|
| `DiscountCalculator.calculate()` | Calculate one article or a collection |
| `DiscountCalculator.calculate_many()` | Calculate a batch with per-item quantities |
| `DiscountCalculator.calculate_dicts()` | Return JSON-ready dictionaries |
| `DiscountCalculator.clear_cache()` | Clear compatibility cache state |
| `BaseGesDiscountCalculator.calculate_article()` | Calculate one article using BaseGes |
| `BaseGesDiscountCalculator.calculate_articles()` | Calculate a batch using BaseGes |
| `BaseGesPricingRepository.get_product(s)` | Read one or many articles |
| `BaseGesPricingRepository.get_client()` | Read customer data |
| `BaseGesPricingRepository.get_discount_*()` | Read discount-table data |
| `BaseGesPricingRepository.get_active_promotions()` | Read active promotions |
| `BaseGesPricingRepository.get_promotion_*()` | Read promotion rules |
| `BaseGesPricingRepository.get_exception_*()` | Read customer exceptions |

## Quick start

```python
from datetime import date
import pymssql
from bg4h_discount_api import BaseGesDiscountCalculator

connection = pymssql.connect(server="server", user="user", password="password", database="company")
try:
    calculator = BaseGesDiscountCalculator(connection)
    result = calculator.calculate_article("ART-001", "000123", 5, price_date=date.today())
    print(result.final_unit_price, result.final_amount)
finally:
    connection.close()
```

## Batch pricing

```python
results = calculator.calculate_articles(
    [{"reference": "ART-001", "quantity": 1}, {"reference": "ART-002", "quantity": 3}],
    client_code="000123",
)
```

Batch methods preserve input order, repeated references, individual quantities, and missing articles while grouping compatible reads.

## Examples for every public method

```python
from datetime import date
from decimal import Decimal
from bg4h_discount_api import (
    ArticleRequest, ClientData, DiscountCalculator, DiscountLine, ProductData,
    Promotion, PromotionInfo, BaseGesPricingRepository,
    matching_discount, matching_specificity, parse_quantity,
    promotion_audience_specificity,
)

calculator = DiscountCalculator(my_repository)
one = calculator.calculate("ART-001", "000123", 2)
many = calculator.calculate_many(["ART-001", {"reference": "ART-002", "quantity": 4}], "000123")
payload = calculator.calculate_dicts("ART-001", "000123")
calculator.clear_cache()

repository = BaseGesPricingRepository(connection)
article = repository.get_product("ART-001")
articles = repository.get_products(["ART-001", "ART-002"])
customer = repository.get_client("000123")
table = repository.get_discount_header("D1")
table_lines = repository.get_discount_lines("D1")
promotions = repository.get_active_promotions(date.today())
promotion_lines = repository.get_promotion_lines("P1")
promotion_lines_many = repository.get_promotion_lines_many(["P1", "P2"])
exception = repository.get_exception_header("000123")
exception_lines = repository.get_exception_lines("E1")

request = ArticleRequest.from_value({"reference": "ART-001", "quantity": "5"})
quantity = parse_quantity("2.5")
product = ProductData("ART-001", "Test article", Decimal("100"), family="F1")
line = DiscountLine(Decimal("10"), family="F1")
specificity = matching_specificity(line, product)
discount = matching_discount([line], product, quantity)
customer_data = ClientData("000123", client_type="TYPE-1")
promotion = Promotion("P1", "Offer", date.today(), date.today(), "TYPE-1")
audience_level = promotion_audience_specificity(promotion, customer_data)
info = PromotionInfo("P1", "Offer", "2026-01-01", "2026-12-31", Decimal("1"), Decimal("0"), Decimal("10"), None, True, True, True)
info_payload = info.to_dict()
result_payload = one.to_dict()
article_has_price = one.found
```

`resolve_priority()` is an internal engine function invoked by the calculator because it uses internal candidate objects.

## Pricing result and priority rules

`PricingResult` exposes `original_price`, `price_before_discount`, `discount_percentage`, `final_unit_price`, `final_amount`, `origin`, promotion/table/exception codes, and `promotions`. `to_dict()` converts `Decimal` values to JSON-compatible numbers.

The existing BaseGes priority order is preserved: customer exceptions (`E`/`E5`), promotions (`P`/`P14`), discount tables (`D`/`D4`), and article discounts (`A`) as fallback. Validity, audience, quantity, minimum amount, net prices, and creation-order tie-breaking follow the ERP rules. The `IMP-04` case is outside the implemented reference logic.

## Custom data sources

Implement `PricingRepository` and inject it into `DiscountCalculator` to use another database or test data. Return exported models such as `ProductData`, `ClientData`, `DiscountLine`, and `PromotionLine`.

## Development and publishing

```bash
python -m pytest --cov=bg4h_discount_api
python -m ruff check src tests
python -m mypy src tests
python -m pip install build twine
python -m build
python -m twine check dist/*
```

On Windows, `build_upload.bat` runs the build, validation, and PyPI upload workflow.
