Metadata-Version: 2.4
Name: dgmaxclient
Version: 1.4.1
Summary: Python SDK for DGMax API - Electronic fiscal document processing for Dominican Republic
Project-URL: Homepage, https://dgmax.do
Project-URL: Documentation, https://docs.dgmax.do
Author-email: DGMax <support@dgmax.do>
License-Expression: MIT
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.10
Requires-Dist: api-client>=1.3.1
Requires-Dist: email-validator>=2.0
Requires-Dist: pydantic>=2.0
Requires-Dist: requests>=2.28.0
Requires-Dist: tenacity>=8.0
Provides-Extra: dev
Requires-Dist: httpx>=0.24.0; extra == 'dev'
Requires-Dist: mypy>=1.0; extra == 'dev'
Requires-Dist: pytest-cov>=4.0; extra == 'dev'
Requires-Dist: pytest>=7.0; extra == 'dev'
Requires-Dist: respx>=0.20; extra == 'dev'
Requires-Dist: ruff>=0.1; extra == 'dev'
Description-Content-Type: text/markdown

# DGMaxClient

Python SDK for the **DGMax API** — electronic fiscal document (e-CF) processing for the Dominican Republic.

Build, validate, and submit e-CF payloads (E31–E47) against DGII's schema with typed Pydantic models, automatic retries, and a clean resource-oriented API. Skip the XSD, the hand-built JSON, and the DGII error decoding.

## Installation

```bash
pip install dgmaxclient
```

## Quick Start

```python
from dgmaxclient import (
    DGMaxClient,
    DocumentCreateRequest,
    CompanyRef,
    ECFPayload,
    ECFEncabezado,
    ECFIdDoc,
    ECFEmisor,
    ECFComprador,
    ECFTotales,
    ECFDetallesItems,
    ECFItem,
)

# Initialize the client
client = DGMaxClient(api_key="dgmax_xxx")

# List companies
companies = client.companies.list()
for company in companies.results:
    print(f"{company.name} ({company.rnc})")

# Create an invoice (E32) — fully typed with IDE autocomplete
invoice = client.invoices.create(
    DocumentCreateRequest(
        company=CompanyRef(id="your-company-uuid"),
        ecf=ECFPayload(
            encabezado=ECFEncabezado(
                id_doc=ECFIdDoc(tipo_ecf=32, e_ncf="E320000000001"),
                emisor=ECFEmisor(
                    rnc_emisor="123456789",
                    razon_social_emisor="Mi Empresa SRL",
                    fecha_emision="15-01-2026",
                ),
                comprador=ECFComprador(
                    razon_social_comprador="Cliente Final",
                ),
                totales=ECFTotales(
                    monto_gravado_i1="847.46",
                    itbis1="18",
                    total_itbis1="152.54",
                    total_itbis="152.54",
                    monto_gravado_total="847.46",
                    monto_total="1000.00",
                ),
            ),
            detalles_items=ECFDetallesItems(item=[
                ECFItem(
                    numero_linea="1",
                    indicador_facturacion=1,
                    nombre_item="Servicio de consultoría",
                    cantidad_item="1",
                    precio_unitario_item="847.46",
                    monto_item="847.46",
                ),
            ]),
        ),
    )
)

print(f"Invoice created: {invoice.encf}")
print(f"Status: {invoice.status}")  # DocumentStatus.REGISTERED → PROCESSING → COMPLETED
```

## Architecture

DGMaxClient uses the **Facade Pattern** with **Composition** to provide a clean, namespaced API. The main client acts as a single entry point that composes multiple resource objects, each responsible for a specific API domain.

```
┌─────────────────┐
│   DGMaxClient   │ ◄── Facade (single entry point)
├─────────────────┤
│ .companies      │───► CompaniesResource
│ .invoices       │───► InvoicesResource
│ .purchases      │───► PurchasesResource
│ .credit_notes   │───► CreditNotesResource
│ ...             │───► (other resources)
└─────────────────┘
         │
         ▼
   HTTP/Auth layer (shared)
```

This pattern enables:
- **Clean API**: `client.companies.list()`, `client.invoices.create({...})`
- **Shared configuration**: All resources use the same authentication and settings
- **Separation of concerns**: Each resource handles its own domain logic
- **Discoverability**: IDE autocomplete works seamlessly

## Features

### Document Types (E31-E47)

The SDK supports all Dominican Republic electronic fiscal document types:

| Type | Resource | Description |
|------|----------|-------------|
| E31 | `client.fiscal_invoices` | Factura de Crédito Fiscal Electrónica |
| E32 | `client.invoices` | Factura de Consumo Electrónica |
| E33 | `client.debit_notes` | Nota de Débito Electrónica |
| E34 | `client.credit_notes` | Nota de Crédito Electrónica |
| E41 | `client.purchases` | Comprobante Electrónico de Compras |
| E43 | `client.minor_expenses` | Comprobante Electrónico para Gastos Menores |
| E44 | `client.special_regimes` | Comprobante Electrónico para Regímenes Especiales |
| E45 | `client.governmental` | Comprobante Electrónico Gubernamental |
| E46 | `client.exports` | Comprobante Electrónico para Exportaciones |
| E47 | `client.payments_abroad` | Comprobante Electrónico para Pagos al Exterior |

### Company Management

```python
from dgmaxclient import CompanyCreate, CertificateCreate

# List companies
companies = client.companies.list()

# Get a specific company
company = client.companies.get("company-uuid")

# Create a company with certificate
company = client.companies.create(CompanyCreate(
    name="Mi Empresa SRL",
    trade_name="Mi Empresa",
    rnc="123456789",
    address="Calle Principal #123",
    certificate=CertificateCreate(
        name="certificate",
        extension="p12",
        content="base64-encoded-certificate",
        password="certificate-password"
    )
))

# Update a company
company = client.companies.update("company-uuid", {
    "phone": "809-555-1234",
    "email": "info@miempresa.com"
})
```

### Document Operations

```python
from dgmaxclient import DocumentFilters, DocumentStatus, PaginationParams

# List documents with pagination
invoices = client.invoices.list(
    params=PaginationParams(limit=50, offset=0)
)

# List documents with filters
invoices = client.invoices.list(
    filters=DocumentFilters(
        status=DocumentStatus.COMPLETED,
        date_from="2024-01-01",
        date_to="2024-12-31",
        search="123456789"  # Search by RNC or eNCF
    )
)

# Get a specific document
invoice = client.invoices.get("document-uuid")

# Create a document (see Quick Start for typed payload example)
invoice = client.invoices.create(payload)
```

### Received Documents (Receptor Module)

```python
# List received documents
received = client.received_documents.list()

# List with filters
from dgmaxclient import ReceivedDocumentFilters

received = client.received_documents.list(
    filters=ReceivedDocumentFilters(
        status="PENDING",
        date_from="2024-01-01",
    )
)

# Approve a received document
response = client.received_documents.approve("document-uuid")
if response.success:
    print(f"Document approved: {response.approval_id}")

# Reject a received document
response = client.received_documents.reject(
    "document-uuid",
    rejection_reason="Invoice amount does not match purchase order"
)

# List commercial approvals received
approvals = client.received_documents.list_commercial_approvals()
```

## Error Handling

The SDK provides a comprehensive exception hierarchy:

```python
from dgmaxclient import (
    DGMaxError,
    DGMaxAuthenticationError,
    DGMaxValidationError,
    DGMaxRequestError,
    DGMaxServerError,
    DGMaxTimeoutError,
    DGMaxConnectionError,
    DGMaxRateLimitError,
)

try:
    invoice = client.invoices.create(payload)
except DGMaxAuthenticationError as e:
    print(f"Authentication failed: {e.message}")
    print(f"Status code: {e.status_code}")
except DGMaxValidationError as e:
    print(f"Validation error: {e.message}")
    if e.response:
        print(f"Details: {e.response}")
except DGMaxServerError as e:
    print(f"Server error (will retry): {e.message}")
except DGMaxTimeoutError:
    print("Request timed out")
except DGMaxConnectionError:
    print("Connection error - check your network")
except DGMaxRateLimitError as e:
    print(f"Rate limited. Retry after: {e.retry_after}s")
except DGMaxError as e:
    print(f"General error: {e.message}")
```

## Configuration

```python
# Custom base URL (e.g. dev environment)
client = DGMaxClient(
    api_key="dgmax_xxx",
    base_url="https://dev.dgmax.do",
)

# Custom timeout (default: 30 seconds)
client = DGMaxClient(
    api_key="dgmax_xxx",
    timeout=60,
)
```

## Automatic Retries

The SDK automatically retries requests on:
- Server errors (5xx)
- Connection errors
- Timeout errors

Retries use exponential backoff with jitter (max 3 attempts).

## Type Safety

All responses are validated with Pydantic models:

```python
from dgmaxclient import ElectronicDocument, DocumentStatus

invoice: ElectronicDocument = client.invoices.get("uuid")

# Type-safe access
print(invoice.id)
print(invoice.encf)
print(invoice.status)  # DocumentStatus enum

if invoice.status == DocumentStatus.COMPLETED:
    print("Document processed successfully")
```

### Typed ECF submodels

Nested ECF fields — `tabla_sub_descuento`, `otra_moneda_detalle`, `retencion`, `otra_moneda`, `transporte`, `informaciones_adicionales`, `subtotales`, `descuentos_o_recargos`, `paginacion`, `tabla_formas_pago`, `tabla_impuesto_adicional`, `tabla_telefono_emisor`, `mineria` — are strongly typed. Typos, wrong nesting, and shape errors raise `ValidationError` at construction time instead of reaching DGII.

**Dict-in still works.** Build sub-objects as plain dicts — the SDK coerces them into the typed classes on the way in, so existing call sites get validation without changes:

```python
ECFItem(
    numero_linea="1",
    indicador_facturacion=1,
    nombre_item="Servicio",
    monto_item="1800.00",
    tabla_sub_descuento={
        "sub_descuento": [
            {"tipo_sub_descuento": "%", "sub_descuento_porcentaje": "10.00"},
        ],
    },
    otra_moneda_detalle={
        "precio_otra_moneda": "100.00",
        "descuento_otra_moneda": "10.00",
        "monto_item_otra_moneda": "90.00",
    },
)

# Typos and wrong shapes now fail fast:
ECFItem(..., tabla_sub_descuento=[{"sub_descuento_porcentaje": 10.0}])
# → ValidationError: tabla_sub_descuento must be a dict with sub_descuento key
```

**Numeric inputs are coerced.** Money fields (`monto_item`, `itbis1`, `monto_total`, `precio_unitario_item`, ...) and line numbers accept `int`, `float`, `Decimal`, or `str`, and serialize to the DGII-compliant string pattern automatically:

```python
from decimal import Decimal

ECFTotales(monto_total=1000, itbis1=Decimal("152.54"))
# → monto_total="1000.00", itbis1="152.54"

ECFItem(numero_linea=1, ...)  # → numero_linea="1"
```

## Requirements

- Python 3.10+
- api-client >= 1.3.1
- pydantic >= 2.0
- tenacity >= 8.0
- requests >= 2.28.0

## License

MIT License - see LICENSE file for details.
