# This file was auto-generated by Fern from our API Definition.

import datetime as dt
import typing

from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper
from ..core.request_options import RequestOptions
from ..types.catalog_allowance_list_response import CatalogAllowanceListResponse
from ..types.catalog_allowance_object import CatalogAllowanceObject
from ..types.catalog_bulk_response import CatalogBulkResponse
from ..types.catalog_entry_list_response import CatalogEntryListResponse
from ..types.catalog_entry_object import CatalogEntryObject
from ..types.catalog_price_item import CatalogPriceItem
from ..types.invoice_item_variance_list_response import InvoiceItemVarianceListResponse
from ..types.variance_list_response import VarianceListResponse
from .raw_client import AsyncRawCatalogClient, RawCatalogClient
from .types.catalog_allowance_bulk_create_request_allowances_item import CatalogAllowanceBulkCreateRequestAllowancesItem
from .types.catalog_entry_bulk_create_request_entries_item import CatalogEntryBulkCreateRequestEntriesItem
from .types.catalog_prices_bulk_create_request_prices_item import CatalogPricesBulkCreateRequestPricesItem
from .types.get_catalog_prices_request_price_type import GetCatalogPricesRequestPriceType
from .types.get_catalog_prices_response import GetCatalogPricesResponse
from .types.get_catalog_variances_request_exception_type import GetCatalogVariancesRequestExceptionType
from .types.patch_catalog_allowances_id_request_allowance_type import PatchCatalogAllowancesIdRequestAllowanceType
from .types.patch_catalog_entries_id_request_properties import PatchCatalogEntriesIdRequestProperties
from .types.post_catalog_allowances_request_allowance_type import PostCatalogAllowancesRequestAllowanceType
from .types.post_catalog_entries_request_allowances_item import PostCatalogEntriesRequestAllowancesItem
from .types.post_catalog_entries_request_prices_item import PostCatalogEntriesRequestPricesItem
from .types.post_catalog_prices_bulk_response import PostCatalogPricesBulkResponse
from .types.post_catalog_prices_request_price_type import PostCatalogPricesRequestPriceType

# this is used as the default value for optional parameters
OMIT = typing.cast(typing.Any, ...)


class CatalogClient:
    def __init__(self, *, client_wrapper: SyncClientWrapper):
        self._raw_client = RawCatalogClient(client_wrapper=client_wrapper)

    @property
    def with_raw_response(self) -> RawCatalogClient:
        """
        Retrieves a raw implementation of this client that returns raw responses.

        Returns
        -------
        RawCatalogClient
        """
        return self._raw_client

    def get_catalog_entries(
        self,
        *,
        ottimate_company_id: int,
        erp_vendor_id: typing.Optional[str] = None,
        search: typing.Optional[str] = None,
        page: typing.Optional[int] = None,
        limit: typing.Optional[int] = None,
        expand: typing.Optional[str] = None,
        request_options: typing.Optional[RequestOptions] = None,
    ) -> CatalogEntryListResponse:
        """
        Returns a paginated list of catalog entries, filterable by company, vendor, and search term.

        Parameters
        ----------
        ottimate_company_id : int
            Filter entries by company ID (required)

        erp_vendor_id : typing.Optional[str]
            Filter entries by vendor name

        search : typing.Optional[str]
            Search term based on item description or name

        page : typing.Optional[int]
            Page number (default: 1)

        limit : typing.Optional[int]
            Items per page (default: 50, max: 100)

        expand : typing.Optional[str]
            Comma-separated list of fields to expand. Options: prices, allowances. When used, returns full CatalogEntryObject instead of CatalogEntryBasicObject.

        request_options : typing.Optional[RequestOptions]
            Request-specific configuration.

        Returns
        -------
        CatalogEntryListResponse
            Successful response with paginated catalog entry list

        Examples
        --------
        from ottimate import Ottimate

        client = Ottimate(
            api_key="YOUR_API_KEY",
            api_version="YOUR_API_VERSION",
            api_key="YOUR_API_KEY",
        )
        client.catalog.get_catalog_entries(
            ottimate_company_id=15196,
            page=1,
            limit=50,
            expand="prices,allowances",
        )
        """
        _response = self._raw_client.get_catalog_entries(
            ottimate_company_id=ottimate_company_id,
            erp_vendor_id=erp_vendor_id,
            search=search,
            page=page,
            limit=limit,
            expand=expand,
            request_options=request_options,
        )
        return _response.data

    def post_catalog_entries(
        self,
        *,
        ottimate_company_id: int,
        erp_vendor_id: str,
        description: str,
        original_description: typing.Optional[str] = OMIT,
        sku: typing.Optional[str] = OMIT,
        upc: typing.Optional[str] = OMIT,
        size: typing.Optional[str] = OMIT,
        uom: typing.Optional[str] = OMIT,
        price: typing.Optional[float] = OMIT,
        unit_price: typing.Optional[float] = OMIT,
        is_split_case: typing.Optional[bool] = OMIT,
        authorized_vendor: typing.Optional[bool] = OMIT,
        catalog_unique_key: typing.Optional[str] = OMIT,
        last_purchased_price: typing.Optional[float] = OMIT,
        reference_id: typing.Optional[str] = OMIT,
        dimensions: typing.Optional[typing.Dict[str, typing.Any]] = OMIT,
        properties: typing.Optional[typing.Dict[str, typing.Any]] = OMIT,
        prices: typing.Optional[typing.Sequence[PostCatalogEntriesRequestPricesItem]] = OMIT,
        allowances: typing.Optional[typing.Sequence[PostCatalogEntriesRequestAllowancesItem]] = OMIT,
        request_options: typing.Optional[RequestOptions] = None,
    ) -> CatalogEntryObject:
        """
        Creates a new catalog entry or updates an existing one if catalog_unique_key matches.

        Parameters
        ----------
        ottimate_company_id : int
            The Ottimate company ID this catalog entry belongs to

        erp_vendor_id : str
            ERP Vendor id from ERP/accounting system

        description : str
            Item name/description for the catalog entry

        original_description : typing.Optional[str]
            Original item description from source system

        sku : typing.Optional[str]
            SKU code

        upc : typing.Optional[str]
            UPC code

        size : typing.Optional[str]
            Pack size (e.g., '6 CT', '10 LB')

        uom : typing.Optional[str]
            Unit of measure (e.g., 'CASE', 'EACH', 'LB')

        price : typing.Optional[float]
            Price for the pack size

        unit_price : typing.Optional[float]
            Price per individual unit within the pack

        is_split_case : typing.Optional[bool]
            Whether items can be purchased in split case quantities

        authorized_vendor : typing.Optional[bool]
            Whether the vendor is authorized for this item

        catalog_unique_key : typing.Optional[str]
            Unique key used to match existing entries for upsert. If omitted, the API auto-generates a key from erp_vendor_id, sku, size, and description. Key normalization: the value is derived from the entry's core identifying fields — the key returned in the response may differ from what you submitted. Always store and reuse the returned catalog_unique_key. Key regeneration: modifying erp_vendor_id, sku, size, or description will generate a new key that no longer matches the existing entry — the API will create a new entry instead of updating the old one, leaving the previous entry as an orphan. To avoid duplicates, store the latest catalog_unique_key from the response and include it in subsequent requests.

        last_purchased_price : typing.Optional[float]
            Last purchased price for this item

        reference_id : typing.Optional[str]
            External reference ID for tracking

        dimensions : typing.Optional[typing.Dict[str, typing.Any]]
            Dimension values keyed by type (e.g. PROJECT, CLASS). Each value is the ERP external_id of the dimension. For existing entries (matched via catalog_unique_key), REPLACES all existing dimensions — include every dimension you want to keep. For new entries, sets the initial dimensions. Omit the field or pass null to leave dimensions unchanged (updates) or create without dimensions (new entries). Passing {} returns 400.

        properties : typing.Optional[typing.Dict[str, typing.Any]]
            Additional custom properties as key-value pairs

        prices : typing.Optional[typing.Sequence[PostCatalogEntriesRequestPricesItem]]
            Nested prices to create with this catalog entry

        allowances : typing.Optional[typing.Sequence[PostCatalogEntriesRequestAllowancesItem]]
            Nested allowances to create with this catalog entry

        request_options : typing.Optional[RequestOptions]
            Request-specific configuration.

        Returns
        -------
        CatalogEntryObject
            Catalog entry created successfully

        Examples
        --------
        from ottimate import Ottimate

        client = Ottimate(
            api_key="YOUR_API_KEY",
            api_version="YOUR_API_VERSION",
            api_key="YOUR_API_KEY",
        )
        client.catalog.post_catalog_entries(
            ottimate_company_id=15196,
            erp_vendor_id="Sysco Foods",
            description="Roma Tomatoes 6CT",
            sku="SKU-TOM-001",
            size="6 CT",
            uom="CASE",
            price=24.99,
            reference_id="sysco-tomato",
        )
        """
        _response = self._raw_client.post_catalog_entries(
            ottimate_company_id=ottimate_company_id,
            erp_vendor_id=erp_vendor_id,
            description=description,
            original_description=original_description,
            sku=sku,
            upc=upc,
            size=size,
            uom=uom,
            price=price,
            unit_price=unit_price,
            is_split_case=is_split_case,
            authorized_vendor=authorized_vendor,
            catalog_unique_key=catalog_unique_key,
            last_purchased_price=last_purchased_price,
            reference_id=reference_id,
            dimensions=dimensions,
            properties=properties,
            prices=prices,
            allowances=allowances,
            request_options=request_options,
        )
        return _response.data

    def get_catalog_entries_id(
        self, id: str, *, request_options: typing.Optional[RequestOptions] = None
    ) -> CatalogEntryObject:
        """
        Returns detailed information for a single catalog entry.

        Parameters
        ----------
        id : str
            The unique ID of the catalog entry

        request_options : typing.Optional[RequestOptions]
            Request-specific configuration.

        Returns
        -------
        CatalogEntryObject
            Successful response with catalog entry details

        Examples
        --------
        from ottimate import Ottimate

        client = Ottimate(
            api_key="YOUR_API_KEY",
            api_version="YOUR_API_VERSION",
            api_key="YOUR_API_KEY",
        )
        client.catalog.get_catalog_entries_id(
            id="racc_90832490382904802384",
        )
        """
        _response = self._raw_client.get_catalog_entries_id(id, request_options=request_options)
        return _response.data

    def patch_catalog_entries_id(
        self,
        id: str,
        *,
        erp_vendor_id: typing.Optional[str] = OMIT,
        description: typing.Optional[str] = OMIT,
        original_description: typing.Optional[str] = OMIT,
        sku: typing.Optional[str] = OMIT,
        upc: typing.Optional[str] = OMIT,
        size: typing.Optional[str] = OMIT,
        uom: typing.Optional[str] = OMIT,
        price: typing.Optional[float] = OMIT,
        unit_price: typing.Optional[float] = OMIT,
        is_split_case: typing.Optional[bool] = OMIT,
        authorized_vendor: typing.Optional[bool] = OMIT,
        dimensions: typing.Optional[typing.Dict[str, typing.Any]] = OMIT,
        properties: typing.Optional[PatchCatalogEntriesIdRequestProperties] = OMIT,
        request_options: typing.Optional[RequestOptions] = None,
    ) -> CatalogEntryObject:
        """
        Updates an existing catalog entry by ID.

        Parameters
        ----------
        id : str
            The unique ID of the catalog entry

        erp_vendor_id : typing.Optional[str]
            ERP Vendor id from ERP/accounting system

        description : typing.Optional[str]
            Item name/description

        original_description : typing.Optional[str]
            Original item description

        sku : typing.Optional[str]
            SKU code

        upc : typing.Optional[str]
            UPC code

        size : typing.Optional[str]
            Pack size

        uom : typing.Optional[str]
            Unit of measure

        price : typing.Optional[float]
            Price

        unit_price : typing.Optional[float]
            Unit price

        is_split_case : typing.Optional[bool]
            Split case indicator

        authorized_vendor : typing.Optional[bool]
            Whether vendor is authorized

        dimensions : typing.Optional[typing.Dict[str, typing.Any]]
            Dimension values keyed by type (e.g. PROJECT, CLASS). Each value is the ERP external_id of the dimension. When provided, REPLACES all existing dimensions on the entry — include every dimension you want to keep. Omit the field or pass null to leave dimensions unchanged. Passing {} returns 400.

        properties : typing.Optional[PatchCatalogEntriesIdRequestProperties]
            Additional properties

        request_options : typing.Optional[RequestOptions]
            Request-specific configuration.

        Returns
        -------
        CatalogEntryObject
            Catalog entry updated successfully

        Examples
        --------
        from ottimate import Ottimate

        client = Ottimate(
            api_key="YOUR_API_KEY",
            api_version="YOUR_API_VERSION",
            api_key="YOUR_API_KEY",
        )
        client.catalog.patch_catalog_entries_id(
            id="racc_90832490382904802384",
            description="Roma Tomatoes Premium 6CT",
            price=26.99,
            size="6 CT",
        )
        """
        _response = self._raw_client.patch_catalog_entries_id(
            id,
            erp_vendor_id=erp_vendor_id,
            description=description,
            original_description=original_description,
            sku=sku,
            upc=upc,
            size=size,
            uom=uom,
            price=price,
            unit_price=unit_price,
            is_split_case=is_split_case,
            authorized_vendor=authorized_vendor,
            dimensions=dimensions,
            properties=properties,
            request_options=request_options,
        )
        return _response.data

    def post_catalog_entries_bulk(
        self,
        *,
        ottimate_company_id: int,
        entries: typing.Sequence[CatalogEntryBulkCreateRequestEntriesItem],
        request_options: typing.Optional[RequestOptions] = None,
    ) -> CatalogBulkResponse:
        """
        Creates or updates multiple catalog entries in a single request. Uses upsert logic based on catalog_unique_key. Maximum 1000 records per request.

        Outcomes per entry:
        - **created**: New entry inserted
        - **updated**: Existing entry found and field changes detected
        - **skipped**: Existing entry found with no field changes — returns HTTP 200 with skipped_count > 0 (not an error)
        - **error**: Validation or integrity failure

        When catalog_unique_key is omitted, the API auto-generates one from erp_vendor_id, sku, size, and description. Always store the catalog_unique_key returned in the response for subsequent upsert calls.

        Parameters
        ----------
        ottimate_company_id : int
            Company ID

        entries : typing.Sequence[CatalogEntryBulkCreateRequestEntriesItem]
            List of entries to create/update (max 1000)

        request_options : typing.Optional[RequestOptions]
            Request-specific configuration.

        Returns
        -------
        CatalogBulkResponse
            All entries were already up-to-date — no changes detected

        Examples
        --------
        from ottimate import Ottimate
        from ottimate.catalog import CatalogEntryBulkCreateRequestEntriesItem

        client = Ottimate(
            api_key="YOUR_API_KEY",
            api_version="YOUR_API_VERSION",
            api_key="YOUR_API_KEY",
        )
        client.catalog.post_catalog_entries_bulk(
            ottimate_company_id=15196,
            entries=[
                CatalogEntryBulkCreateRequestEntriesItem(
                    erp_vendor_id="Sysco Foods",
                    description="Fresh Spinach 2LB",
                    sku="SKU-SPN-001",
                    size="2 LB",
                    price=12.99,
                    reference_id="sysco-spinach",
                    catalog_unique_key="example-catalog-key",
                ),
                CatalogEntryBulkCreateRequestEntriesItem(
                    erp_vendor_id="Sysco Foods",
                    description="Baby Carrots 5LB",
                    sku="SKU-CAR-001",
                    size="5 LB",
                    price=18.99,
                    reference_id="sysco-carrot",
                    catalog_unique_key="example-catalog-key",
                ),
            ],
        )
        """
        _response = self._raw_client.post_catalog_entries_bulk(
            ottimate_company_id=ottimate_company_id, entries=entries, request_options=request_options
        )
        return _response.data

    def get_catalog_allowances(
        self,
        *,
        ottimate_company_id: int,
        catalog_entry_id: typing.Optional[str] = None,
        ottimate_location_id: typing.Optional[int] = None,
        effective_date: typing.Optional[str] = None,
        request_options: typing.Optional[RequestOptions] = None,
    ) -> CatalogAllowanceListResponse:
        """
        Returns a paginated list of catalog allowances, filterable by company, catalog entry, location, and effective date.

        Parameters
        ----------
        ottimate_company_id : int
            Filter allowances by company ID (required)

        catalog_entry_id : typing.Optional[str]
            Filter by catalog entry ID

        ottimate_location_id : typing.Optional[int]
            Filter by location ID

        effective_date : typing.Optional[str]
            Filter allowances valid on this date (YYYY-MM-DD)

        request_options : typing.Optional[RequestOptions]
            Request-specific configuration.

        Returns
        -------
        CatalogAllowanceListResponse
            Successful response with paginated allowance list

        Examples
        --------
        from ottimate import Ottimate

        client = Ottimate(
            api_key="YOUR_API_KEY",
            api_version="YOUR_API_VERSION",
            api_key="YOUR_API_KEY",
        )
        client.catalog.get_catalog_allowances(
            ottimate_company_id=15196,
        )
        """
        _response = self._raw_client.get_catalog_allowances(
            ottimate_company_id=ottimate_company_id,
            catalog_entry_id=catalog_entry_id,
            ottimate_location_id=ottimate_location_id,
            effective_date=effective_date,
            request_options=request_options,
        )
        return _response.data

    def post_catalog_allowances(
        self,
        *,
        ottimate_company_id: int,
        catalog_entry_id: int,
        amount: float,
        start_date: dt.datetime,
        allowance_type: typing.Optional[PostCatalogAllowancesRequestAllowanceType] = OMIT,
        unit_allowance: typing.Optional[float] = OMIT,
        target: typing.Optional[int] = OMIT,
        ottimate_location_id: typing.Optional[int] = OMIT,
        ottimate_group_id: typing.Optional[int] = OMIT,
        end_date: typing.Optional[dt.datetime] = OMIT,
        reference_id: typing.Optional[str] = OMIT,
        request_options: typing.Optional[RequestOptions] = None,
    ) -> CatalogAllowanceObject:
        """
        Creates a new catalog allowance for a catalog entry.

        Parameters
        ----------
        ottimate_company_id : int
            The Ottimate company ID this allowance belongs to

        catalog_entry_id : int
            The ID of the catalog entry this allowance applies to

        amount : float
            Allowance amount for the pack size

        start_date : dt.datetime
            Allowance effective start date (ISO 8601)

        allowance_type : typing.Optional[PostCatalogAllowancesRequestAllowanceType]
            Type of allowance (default: 'allowance')

        unit_allowance : typing.Optional[float]
            Allowance amount per individual unit (default: 0)

        target : typing.Optional[int]
            Target level: 10=company (default), 15=location_group, 20=location

        ottimate_location_id : typing.Optional[int]
            Location ID (required when target is 20/location)

        ottimate_group_id : typing.Optional[int]
            Location group ID (required when target is 15/location_group)

        end_date : typing.Optional[dt.datetime]
            Allowance effective end date (ISO 8601, optional)

        reference_id : typing.Optional[str]
            Unique reference ID for upsert operations

        request_options : typing.Optional[RequestOptions]
            Request-specific configuration.

        Returns
        -------
        CatalogAllowanceObject
            Catalog allowance created successfully

        Examples
        --------
        import datetime

        from ottimate import Ottimate

        client = Ottimate(
            api_key="YOUR_API_KEY",
            api_version="YOUR_API_VERSION",
            api_key="YOUR_API_KEY",
        )
        client.catalog.post_catalog_allowances(
            catalog_entry_id=12347,
            ottimate_company_id=15196,
            allowance_type="rebate",
            amount=2.0,
            start_date=datetime.datetime.fromisoformat(
                "2024-12-01 00:00:00+00:00",
            ),
            reference_id="allow-new",
        )
        """
        _response = self._raw_client.post_catalog_allowances(
            ottimate_company_id=ottimate_company_id,
            catalog_entry_id=catalog_entry_id,
            amount=amount,
            start_date=start_date,
            allowance_type=allowance_type,
            unit_allowance=unit_allowance,
            target=target,
            ottimate_location_id=ottimate_location_id,
            ottimate_group_id=ottimate_group_id,
            end_date=end_date,
            reference_id=reference_id,
            request_options=request_options,
        )
        return _response.data

    def patch_catalog_allowances_id(
        self,
        id: str,
        *,
        allowance_type: typing.Optional[PatchCatalogAllowancesIdRequestAllowanceType] = OMIT,
        amount: typing.Optional[float] = OMIT,
        unit_allowance: typing.Optional[float] = OMIT,
        target: typing.Optional[int] = OMIT,
        ottimate_location_id: typing.Optional[int] = OMIT,
        ottimate_group_id: typing.Optional[int] = OMIT,
        start_date: typing.Optional[dt.datetime] = OMIT,
        end_date: typing.Optional[dt.datetime] = OMIT,
        request_options: typing.Optional[RequestOptions] = None,
    ) -> CatalogAllowanceObject:
        """
        Updates an existing catalog allowance by ID.

        Parameters
        ----------
        id : str
            The unique ID of the catalog allowance

        allowance_type : typing.Optional[PatchCatalogAllowancesIdRequestAllowanceType]
            Type of allowance

        amount : typing.Optional[float]
            Allowance amount

        unit_allowance : typing.Optional[float]
            Unit allowance amount

        target : typing.Optional[int]
            Target (10=company, 15=location_group, 20=location)

        ottimate_location_id : typing.Optional[int]
            Location ID

        ottimate_group_id : typing.Optional[int]
            Restaurant group ID

        start_date : typing.Optional[dt.datetime]
            Start date

        end_date : typing.Optional[dt.datetime]
            End date

        request_options : typing.Optional[RequestOptions]
            Request-specific configuration.

        Returns
        -------
        CatalogAllowanceObject
            Catalog allowance updated successfully

        Examples
        --------
        import datetime

        from ottimate import Ottimate

        client = Ottimate(
            api_key="YOUR_API_KEY",
            api_version="YOUR_API_VERSION",
            api_key="YOUR_API_KEY",
        )
        client.catalog.patch_catalog_allowances_id(
            id="racc_90832490382904802384",
            amount=2.5,
            end_date=datetime.datetime.fromisoformat(
                "2025-12-31 23:59:59+00:00",
            ),
        )
        """
        _response = self._raw_client.patch_catalog_allowances_id(
            id,
            allowance_type=allowance_type,
            amount=amount,
            unit_allowance=unit_allowance,
            target=target,
            ottimate_location_id=ottimate_location_id,
            ottimate_group_id=ottimate_group_id,
            start_date=start_date,
            end_date=end_date,
            request_options=request_options,
        )
        return _response.data

    def post_catalog_allowances_bulk(
        self,
        *,
        ottimate_company_id: int,
        allowances: typing.Sequence[CatalogAllowanceBulkCreateRequestAllowancesItem],
        request_options: typing.Optional[RequestOptions] = None,
    ) -> CatalogBulkResponse:
        """
        Creates or updates multiple catalog allowances in a single request. Uses upsert logic based on reference_id. Maximum 1000 records per request.

        Parameters
        ----------
        ottimate_company_id : int
            Company ID

        allowances : typing.Sequence[CatalogAllowanceBulkCreateRequestAllowancesItem]
            List of allowances to create/update (max 1000)

        request_options : typing.Optional[RequestOptions]
            Request-specific configuration.

        Returns
        -------
        CatalogBulkResponse
            All allowances created/updated successfully

        Examples
        --------
        import datetime

        from ottimate import Ottimate
        from ottimate.catalog import CatalogAllowanceBulkCreateRequestAllowancesItem

        client = Ottimate(
            api_key="YOUR_API_KEY",
            api_version="YOUR_API_VERSION",
            api_key="YOUR_API_KEY",
        )
        client.catalog.post_catalog_allowances_bulk(
            ottimate_company_id=15196,
            allowances=[
                CatalogAllowanceBulkCreateRequestAllowancesItem(
                    catalog_entry_id="cte_12348",
                    allowance_type="rebate",
                    amount=1.5,
                    start_date=datetime.datetime.fromisoformat(
                        "2024-12-01 00:00:00+00:00",
                    ),
                    reference_id="allow-bulk",
                ),
                CatalogAllowanceBulkCreateRequestAllowancesItem(
                    catalog_entry_id="cte_12349",
                    allowance_type="discount",
                    amount=2.0,
                    start_date=datetime.datetime.fromisoformat(
                        "2024-12-01 00:00:00+00:00",
                    ),
                    reference_id="allow-bulk",
                ),
            ],
        )
        """
        _response = self._raw_client.post_catalog_allowances_bulk(
            ottimate_company_id=ottimate_company_id, allowances=allowances, request_options=request_options
        )
        return _response.data

    def get_catalog_prices(
        self,
        *,
        ottimate_company_id: int,
        catalog_entry_id: typing.Optional[str] = None,
        ottimate_location_id: typing.Optional[int] = None,
        effective_date: typing.Optional[str] = None,
        price_type: typing.Optional[GetCatalogPricesRequestPriceType] = None,
        request_options: typing.Optional[RequestOptions] = None,
    ) -> GetCatalogPricesResponse:
        """
        Returns a paginated list of catalog prices, filterable by company, catalog entry, location, effective date, and price type.

        Parameters
        ----------
        ottimate_company_id : int
            Filter prices by company ID (required)

        catalog_entry_id : typing.Optional[str]
            Filter by catalog entry ID

        ottimate_location_id : typing.Optional[int]
            Filter by location ID

        effective_date : typing.Optional[str]
            Filter prices valid on this date (YYYY-MM-DD)

        price_type : typing.Optional[GetCatalogPricesRequestPriceType]
            Filter by price type

        request_options : typing.Optional[RequestOptions]
            Request-specific configuration.

        Returns
        -------
        GetCatalogPricesResponse
            Successful response with paginated price list

        Examples
        --------
        from ottimate import Ottimate

        client = Ottimate(
            api_key="YOUR_API_KEY",
            api_version="YOUR_API_VERSION",
            api_key="YOUR_API_KEY",
        )
        client.catalog.get_catalog_prices(
            ottimate_company_id=15196,
        )
        """
        _response = self._raw_client.get_catalog_prices(
            ottimate_company_id=ottimate_company_id,
            catalog_entry_id=catalog_entry_id,
            ottimate_location_id=ottimate_location_id,
            effective_date=effective_date,
            price_type=price_type,
            request_options=request_options,
        )
        return _response.data

    def post_catalog_prices(
        self,
        *,
        ottimate_company_id: int,
        catalog_entry_id: str,
        price: float,
        start_date: dt.datetime,
        unit_price: typing.Optional[float] = OMIT,
        price_type: typing.Optional[PostCatalogPricesRequestPriceType] = OMIT,
        ottimate_location_id: typing.Optional[int] = OMIT,
        ottimate_group_id: typing.Optional[int] = OMIT,
        end_date: typing.Optional[dt.datetime] = OMIT,
        reference_id: typing.Optional[str] = OMIT,
        request_options: typing.Optional[RequestOptions] = None,
    ) -> CatalogPriceItem:
        """
        Creates a new catalog price for a catalog entry.

        Parameters
        ----------
        ottimate_company_id : int
            The Ottimate company ID this price belongs to

        catalog_entry_id : str
            The ID of the catalog entry this price applies to

        price : float
            Price amount for the pack size

        start_date : dt.datetime
            Price effective start date (ISO 8601)

        unit_price : typing.Optional[float]
            Price amount per individual unit (optional)

        price_type : typing.Optional[PostCatalogPricesRequestPriceType]
            Price type: company (default), location_group, or location

        ottimate_location_id : typing.Optional[int]
            Location ID (required when price_type is 'location')

        ottimate_group_id : typing.Optional[int]
            Location group ID (required when price_type is 'location_group')

        end_date : typing.Optional[dt.datetime]
            Price effective end date (ISO 8601, optional)

        reference_id : typing.Optional[str]
            External reference ID for the price

        request_options : typing.Optional[RequestOptions]
            Request-specific configuration.

        Returns
        -------
        CatalogPriceItem
            Price created successfully

        Examples
        --------
        import datetime

        from ottimate import Ottimate

        client = Ottimate(
            api_key="YOUR_API_KEY",
            api_version="YOUR_API_VERSION",
            api_key="YOUR_API_KEY",
        )
        client.catalog.post_catalog_prices(
            ottimate_company_id=15196,
            catalog_entry_id="example_catalog_entry_id",
            price=100.0,
            start_date=datetime.datetime.fromisoformat(
                "2024-12-05 10:30:00+00:00",
            ),
        )
        """
        _response = self._raw_client.post_catalog_prices(
            ottimate_company_id=ottimate_company_id,
            catalog_entry_id=catalog_entry_id,
            price=price,
            start_date=start_date,
            unit_price=unit_price,
            price_type=price_type,
            ottimate_location_id=ottimate_location_id,
            ottimate_group_id=ottimate_group_id,
            end_date=end_date,
            reference_id=reference_id,
            request_options=request_options,
        )
        return _response.data

    def post_catalog_prices_bulk(
        self,
        *,
        ottimate_company_id: int,
        prices: typing.Sequence[CatalogPricesBulkCreateRequestPricesItem],
        request_options: typing.Optional[RequestOptions] = None,
    ) -> PostCatalogPricesBulkResponse:
        """
        Creates multiple catalog prices in a single request. Unlike allowances, this endpoint does not support upsert - duplicate reference_ids will be rejected. Maximum 1000 records per request.

        Parameters
        ----------
        ottimate_company_id : int
            Company ID

        prices : typing.Sequence[CatalogPricesBulkCreateRequestPricesItem]
            List of prices to create (max 1000)

        request_options : typing.Optional[RequestOptions]
            Request-specific configuration.

        Returns
        -------
        PostCatalogPricesBulkResponse
            All prices created successfully

        Examples
        --------
        import datetime

        from ottimate import Ottimate
        from ottimate.catalog import CatalogPricesBulkCreateRequestPricesItem

        client = Ottimate(
            api_key="YOUR_API_KEY",
            api_version="YOUR_API_VERSION",
            api_key="YOUR_API_KEY",
        )
        client.catalog.post_catalog_prices_bulk(
            ottimate_company_id=15196,
            prices=[
                CatalogPricesBulkCreateRequestPricesItem(
                    catalog_entry_id="cte_12345",
                    price=25.99,
                    unit_price=2.16,
                    price_type="company",
                    start_date=datetime.datetime.fromisoformat(
                        "2024-01-01 00:00:00+00:00",
                    ),
                    reference_id="price-sysco-tom-1",
                )
            ],
        )
        """
        _response = self._raw_client.post_catalog_prices_bulk(
            ottimate_company_id=ottimate_company_id, prices=prices, request_options=request_options
        )
        return _response.data

    def get_catalog_variances(
        self,
        *,
        ottimate_company_id: int,
        variance_only: typing.Optional[bool] = None,
        expand: typing.Optional[str] = None,
        invoice_start_date: typing.Optional[str] = None,
        invoice_end_date: typing.Optional[str] = None,
        erp_vendor_ids: typing.Optional[str] = None,
        ottimate_location_ids: typing.Optional[str] = None,
        exception_type: typing.Optional[GetCatalogVariancesRequestExceptionType] = None,
        page: typing.Optional[int] = None,
        limit: typing.Optional[int] = None,
        request_options: typing.Optional[RequestOptions] = None,
    ) -> VarianceListResponse:
        """
        Returns catalog entries with price variance information from invoice items. By default, returns all items that had purchases in the specified period, including items with zero variance. Use variance_only=true to return only entries with non-zero variance. Use expand=invoice_items to include all invoice items inline (otherwise only the most recent invoice item is returned).

        Parameters
        ----------
        ottimate_company_id : int
            Filter by company ID (required)

        variance_only : typing.Optional[bool]
            If true, returns only entries with non-zero variance. Default: false (returns all items including zero variance)

        expand : typing.Optional[str]
            Comma-separated list of fields to expand. Supports 'invoice_items' to include all invoice items for each entry.

        invoice_start_date : typing.Optional[str]
            Start of date range (YYYY-MM-DD)

        invoice_end_date : typing.Optional[str]
            End of date range (YYYY-MM-DD)

        erp_vendor_ids : typing.Optional[str]
            Comma-separated ERP vendor IDs (accounting vendor IDs) to filter variances by vendor.

        ottimate_location_ids : typing.Optional[str]
            Comma-separated location IDs

        exception_type : typing.Optional[GetCatalogVariancesRequestExceptionType]
            Filter by variance type

        page : typing.Optional[int]
            Page number for pagination

        limit : typing.Optional[int]
            Number of items per page

        request_options : typing.Optional[RequestOptions]
            Request-specific configuration.

        Returns
        -------
        VarianceListResponse
            Successful response with paginated variance list

        Examples
        --------
        from ottimate import Ottimate

        client = Ottimate(
            api_key="YOUR_API_KEY",
            api_version="YOUR_API_VERSION",
            api_key="YOUR_API_KEY",
        )
        client.catalog.get_catalog_variances(
            ottimate_company_id=15196,
            variance_only=False,
            page=1,
            limit=20,
        )
        """
        _response = self._raw_client.get_catalog_variances(
            ottimate_company_id=ottimate_company_id,
            variance_only=variance_only,
            expand=expand,
            invoice_start_date=invoice_start_date,
            invoice_end_date=invoice_end_date,
            erp_vendor_ids=erp_vendor_ids,
            ottimate_location_ids=ottimate_location_ids,
            exception_type=exception_type,
            page=page,
            limit=limit,
            request_options=request_options,
        )
        return _response.data

    def get_catalog_variances_id_invoice_items(
        self,
        id: str,
        *,
        page: typing.Optional[int] = None,
        limit: typing.Optional[int] = None,
        request_options: typing.Optional[RequestOptions] = None,
    ) -> InvoiceItemVarianceListResponse:
        """
        Returns paginated invoice items with price variances for a specific catalog entry. Use this endpoint when the expand=invoice_items response indicates has_more=true.

        Parameters
        ----------
        id : str
            The catalog entry ID

        page : typing.Optional[int]
            Page number

        limit : typing.Optional[int]
            Items per page (default: 20)

        request_options : typing.Optional[RequestOptions]
            Request-specific configuration.

        Returns
        -------
        InvoiceItemVarianceListResponse
            Successful response with paginated invoice items

        Examples
        --------
        from ottimate import Ottimate

        client = Ottimate(
            api_key="YOUR_API_KEY",
            api_version="YOUR_API_VERSION",
            api_key="YOUR_API_KEY",
        )
        client.catalog.get_catalog_variances_id_invoice_items(
            id="racc_90832490382904802384",
            page=1,
            limit=20,
        )
        """
        _response = self._raw_client.get_catalog_variances_id_invoice_items(
            id, page=page, limit=limit, request_options=request_options
        )
        return _response.data


class AsyncCatalogClient:
    def __init__(self, *, client_wrapper: AsyncClientWrapper):
        self._raw_client = AsyncRawCatalogClient(client_wrapper=client_wrapper)

    @property
    def with_raw_response(self) -> AsyncRawCatalogClient:
        """
        Retrieves a raw implementation of this client that returns raw responses.

        Returns
        -------
        AsyncRawCatalogClient
        """
        return self._raw_client

    async def get_catalog_entries(
        self,
        *,
        ottimate_company_id: int,
        erp_vendor_id: typing.Optional[str] = None,
        search: typing.Optional[str] = None,
        page: typing.Optional[int] = None,
        limit: typing.Optional[int] = None,
        expand: typing.Optional[str] = None,
        request_options: typing.Optional[RequestOptions] = None,
    ) -> CatalogEntryListResponse:
        """
        Returns a paginated list of catalog entries, filterable by company, vendor, and search term.

        Parameters
        ----------
        ottimate_company_id : int
            Filter entries by company ID (required)

        erp_vendor_id : typing.Optional[str]
            Filter entries by vendor name

        search : typing.Optional[str]
            Search term based on item description or name

        page : typing.Optional[int]
            Page number (default: 1)

        limit : typing.Optional[int]
            Items per page (default: 50, max: 100)

        expand : typing.Optional[str]
            Comma-separated list of fields to expand. Options: prices, allowances. When used, returns full CatalogEntryObject instead of CatalogEntryBasicObject.

        request_options : typing.Optional[RequestOptions]
            Request-specific configuration.

        Returns
        -------
        CatalogEntryListResponse
            Successful response with paginated catalog entry list

        Examples
        --------
        import asyncio

        from ottimate import AsyncOttimate

        client = AsyncOttimate(
            api_key="YOUR_API_KEY",
            api_version="YOUR_API_VERSION",
            api_key="YOUR_API_KEY",
        )


        async def main() -> None:
            await client.catalog.get_catalog_entries(
                ottimate_company_id=15196,
                page=1,
                limit=50,
                expand="prices,allowances",
            )


        asyncio.run(main())
        """
        _response = await self._raw_client.get_catalog_entries(
            ottimate_company_id=ottimate_company_id,
            erp_vendor_id=erp_vendor_id,
            search=search,
            page=page,
            limit=limit,
            expand=expand,
            request_options=request_options,
        )
        return _response.data

    async def post_catalog_entries(
        self,
        *,
        ottimate_company_id: int,
        erp_vendor_id: str,
        description: str,
        original_description: typing.Optional[str] = OMIT,
        sku: typing.Optional[str] = OMIT,
        upc: typing.Optional[str] = OMIT,
        size: typing.Optional[str] = OMIT,
        uom: typing.Optional[str] = OMIT,
        price: typing.Optional[float] = OMIT,
        unit_price: typing.Optional[float] = OMIT,
        is_split_case: typing.Optional[bool] = OMIT,
        authorized_vendor: typing.Optional[bool] = OMIT,
        catalog_unique_key: typing.Optional[str] = OMIT,
        last_purchased_price: typing.Optional[float] = OMIT,
        reference_id: typing.Optional[str] = OMIT,
        dimensions: typing.Optional[typing.Dict[str, typing.Any]] = OMIT,
        properties: typing.Optional[typing.Dict[str, typing.Any]] = OMIT,
        prices: typing.Optional[typing.Sequence[PostCatalogEntriesRequestPricesItem]] = OMIT,
        allowances: typing.Optional[typing.Sequence[PostCatalogEntriesRequestAllowancesItem]] = OMIT,
        request_options: typing.Optional[RequestOptions] = None,
    ) -> CatalogEntryObject:
        """
        Creates a new catalog entry or updates an existing one if catalog_unique_key matches.

        Parameters
        ----------
        ottimate_company_id : int
            The Ottimate company ID this catalog entry belongs to

        erp_vendor_id : str
            ERP Vendor id from ERP/accounting system

        description : str
            Item name/description for the catalog entry

        original_description : typing.Optional[str]
            Original item description from source system

        sku : typing.Optional[str]
            SKU code

        upc : typing.Optional[str]
            UPC code

        size : typing.Optional[str]
            Pack size (e.g., '6 CT', '10 LB')

        uom : typing.Optional[str]
            Unit of measure (e.g., 'CASE', 'EACH', 'LB')

        price : typing.Optional[float]
            Price for the pack size

        unit_price : typing.Optional[float]
            Price per individual unit within the pack

        is_split_case : typing.Optional[bool]
            Whether items can be purchased in split case quantities

        authorized_vendor : typing.Optional[bool]
            Whether the vendor is authorized for this item

        catalog_unique_key : typing.Optional[str]
            Unique key used to match existing entries for upsert. If omitted, the API auto-generates a key from erp_vendor_id, sku, size, and description. Key normalization: the value is derived from the entry's core identifying fields — the key returned in the response may differ from what you submitted. Always store and reuse the returned catalog_unique_key. Key regeneration: modifying erp_vendor_id, sku, size, or description will generate a new key that no longer matches the existing entry — the API will create a new entry instead of updating the old one, leaving the previous entry as an orphan. To avoid duplicates, store the latest catalog_unique_key from the response and include it in subsequent requests.

        last_purchased_price : typing.Optional[float]
            Last purchased price for this item

        reference_id : typing.Optional[str]
            External reference ID for tracking

        dimensions : typing.Optional[typing.Dict[str, typing.Any]]
            Dimension values keyed by type (e.g. PROJECT, CLASS). Each value is the ERP external_id of the dimension. For existing entries (matched via catalog_unique_key), REPLACES all existing dimensions — include every dimension you want to keep. For new entries, sets the initial dimensions. Omit the field or pass null to leave dimensions unchanged (updates) or create without dimensions (new entries). Passing {} returns 400.

        properties : typing.Optional[typing.Dict[str, typing.Any]]
            Additional custom properties as key-value pairs

        prices : typing.Optional[typing.Sequence[PostCatalogEntriesRequestPricesItem]]
            Nested prices to create with this catalog entry

        allowances : typing.Optional[typing.Sequence[PostCatalogEntriesRequestAllowancesItem]]
            Nested allowances to create with this catalog entry

        request_options : typing.Optional[RequestOptions]
            Request-specific configuration.

        Returns
        -------
        CatalogEntryObject
            Catalog entry created successfully

        Examples
        --------
        import asyncio

        from ottimate import AsyncOttimate

        client = AsyncOttimate(
            api_key="YOUR_API_KEY",
            api_version="YOUR_API_VERSION",
            api_key="YOUR_API_KEY",
        )


        async def main() -> None:
            await client.catalog.post_catalog_entries(
                ottimate_company_id=15196,
                erp_vendor_id="Sysco Foods",
                description="Roma Tomatoes 6CT",
                sku="SKU-TOM-001",
                size="6 CT",
                uom="CASE",
                price=24.99,
                reference_id="sysco-tomato",
            )


        asyncio.run(main())
        """
        _response = await self._raw_client.post_catalog_entries(
            ottimate_company_id=ottimate_company_id,
            erp_vendor_id=erp_vendor_id,
            description=description,
            original_description=original_description,
            sku=sku,
            upc=upc,
            size=size,
            uom=uom,
            price=price,
            unit_price=unit_price,
            is_split_case=is_split_case,
            authorized_vendor=authorized_vendor,
            catalog_unique_key=catalog_unique_key,
            last_purchased_price=last_purchased_price,
            reference_id=reference_id,
            dimensions=dimensions,
            properties=properties,
            prices=prices,
            allowances=allowances,
            request_options=request_options,
        )
        return _response.data

    async def get_catalog_entries_id(
        self, id: str, *, request_options: typing.Optional[RequestOptions] = None
    ) -> CatalogEntryObject:
        """
        Returns detailed information for a single catalog entry.

        Parameters
        ----------
        id : str
            The unique ID of the catalog entry

        request_options : typing.Optional[RequestOptions]
            Request-specific configuration.

        Returns
        -------
        CatalogEntryObject
            Successful response with catalog entry details

        Examples
        --------
        import asyncio

        from ottimate import AsyncOttimate

        client = AsyncOttimate(
            api_key="YOUR_API_KEY",
            api_version="YOUR_API_VERSION",
            api_key="YOUR_API_KEY",
        )


        async def main() -> None:
            await client.catalog.get_catalog_entries_id(
                id="racc_90832490382904802384",
            )


        asyncio.run(main())
        """
        _response = await self._raw_client.get_catalog_entries_id(id, request_options=request_options)
        return _response.data

    async def patch_catalog_entries_id(
        self,
        id: str,
        *,
        erp_vendor_id: typing.Optional[str] = OMIT,
        description: typing.Optional[str] = OMIT,
        original_description: typing.Optional[str] = OMIT,
        sku: typing.Optional[str] = OMIT,
        upc: typing.Optional[str] = OMIT,
        size: typing.Optional[str] = OMIT,
        uom: typing.Optional[str] = OMIT,
        price: typing.Optional[float] = OMIT,
        unit_price: typing.Optional[float] = OMIT,
        is_split_case: typing.Optional[bool] = OMIT,
        authorized_vendor: typing.Optional[bool] = OMIT,
        dimensions: typing.Optional[typing.Dict[str, typing.Any]] = OMIT,
        properties: typing.Optional[PatchCatalogEntriesIdRequestProperties] = OMIT,
        request_options: typing.Optional[RequestOptions] = None,
    ) -> CatalogEntryObject:
        """
        Updates an existing catalog entry by ID.

        Parameters
        ----------
        id : str
            The unique ID of the catalog entry

        erp_vendor_id : typing.Optional[str]
            ERP Vendor id from ERP/accounting system

        description : typing.Optional[str]
            Item name/description

        original_description : typing.Optional[str]
            Original item description

        sku : typing.Optional[str]
            SKU code

        upc : typing.Optional[str]
            UPC code

        size : typing.Optional[str]
            Pack size

        uom : typing.Optional[str]
            Unit of measure

        price : typing.Optional[float]
            Price

        unit_price : typing.Optional[float]
            Unit price

        is_split_case : typing.Optional[bool]
            Split case indicator

        authorized_vendor : typing.Optional[bool]
            Whether vendor is authorized

        dimensions : typing.Optional[typing.Dict[str, typing.Any]]
            Dimension values keyed by type (e.g. PROJECT, CLASS). Each value is the ERP external_id of the dimension. When provided, REPLACES all existing dimensions on the entry — include every dimension you want to keep. Omit the field or pass null to leave dimensions unchanged. Passing {} returns 400.

        properties : typing.Optional[PatchCatalogEntriesIdRequestProperties]
            Additional properties

        request_options : typing.Optional[RequestOptions]
            Request-specific configuration.

        Returns
        -------
        CatalogEntryObject
            Catalog entry updated successfully

        Examples
        --------
        import asyncio

        from ottimate import AsyncOttimate

        client = AsyncOttimate(
            api_key="YOUR_API_KEY",
            api_version="YOUR_API_VERSION",
            api_key="YOUR_API_KEY",
        )


        async def main() -> None:
            await client.catalog.patch_catalog_entries_id(
                id="racc_90832490382904802384",
                description="Roma Tomatoes Premium 6CT",
                price=26.99,
                size="6 CT",
            )


        asyncio.run(main())
        """
        _response = await self._raw_client.patch_catalog_entries_id(
            id,
            erp_vendor_id=erp_vendor_id,
            description=description,
            original_description=original_description,
            sku=sku,
            upc=upc,
            size=size,
            uom=uom,
            price=price,
            unit_price=unit_price,
            is_split_case=is_split_case,
            authorized_vendor=authorized_vendor,
            dimensions=dimensions,
            properties=properties,
            request_options=request_options,
        )
        return _response.data

    async def post_catalog_entries_bulk(
        self,
        *,
        ottimate_company_id: int,
        entries: typing.Sequence[CatalogEntryBulkCreateRequestEntriesItem],
        request_options: typing.Optional[RequestOptions] = None,
    ) -> CatalogBulkResponse:
        """
        Creates or updates multiple catalog entries in a single request. Uses upsert logic based on catalog_unique_key. Maximum 1000 records per request.

        Outcomes per entry:
        - **created**: New entry inserted
        - **updated**: Existing entry found and field changes detected
        - **skipped**: Existing entry found with no field changes — returns HTTP 200 with skipped_count > 0 (not an error)
        - **error**: Validation or integrity failure

        When catalog_unique_key is omitted, the API auto-generates one from erp_vendor_id, sku, size, and description. Always store the catalog_unique_key returned in the response for subsequent upsert calls.

        Parameters
        ----------
        ottimate_company_id : int
            Company ID

        entries : typing.Sequence[CatalogEntryBulkCreateRequestEntriesItem]
            List of entries to create/update (max 1000)

        request_options : typing.Optional[RequestOptions]
            Request-specific configuration.

        Returns
        -------
        CatalogBulkResponse
            All entries were already up-to-date — no changes detected

        Examples
        --------
        import asyncio

        from ottimate import AsyncOttimate
        from ottimate.catalog import CatalogEntryBulkCreateRequestEntriesItem

        client = AsyncOttimate(
            api_key="YOUR_API_KEY",
            api_version="YOUR_API_VERSION",
            api_key="YOUR_API_KEY",
        )


        async def main() -> None:
            await client.catalog.post_catalog_entries_bulk(
                ottimate_company_id=15196,
                entries=[
                    CatalogEntryBulkCreateRequestEntriesItem(
                        erp_vendor_id="Sysco Foods",
                        description="Fresh Spinach 2LB",
                        sku="SKU-SPN-001",
                        size="2 LB",
                        price=12.99,
                        reference_id="sysco-spinach",
                        catalog_unique_key="example-catalog-key",
                    ),
                    CatalogEntryBulkCreateRequestEntriesItem(
                        erp_vendor_id="Sysco Foods",
                        description="Baby Carrots 5LB",
                        sku="SKU-CAR-001",
                        size="5 LB",
                        price=18.99,
                        reference_id="sysco-carrot",
                        catalog_unique_key="example-catalog-key",
                    ),
                ],
            )


        asyncio.run(main())
        """
        _response = await self._raw_client.post_catalog_entries_bulk(
            ottimate_company_id=ottimate_company_id, entries=entries, request_options=request_options
        )
        return _response.data

    async def get_catalog_allowances(
        self,
        *,
        ottimate_company_id: int,
        catalog_entry_id: typing.Optional[str] = None,
        ottimate_location_id: typing.Optional[int] = None,
        effective_date: typing.Optional[str] = None,
        request_options: typing.Optional[RequestOptions] = None,
    ) -> CatalogAllowanceListResponse:
        """
        Returns a paginated list of catalog allowances, filterable by company, catalog entry, location, and effective date.

        Parameters
        ----------
        ottimate_company_id : int
            Filter allowances by company ID (required)

        catalog_entry_id : typing.Optional[str]
            Filter by catalog entry ID

        ottimate_location_id : typing.Optional[int]
            Filter by location ID

        effective_date : typing.Optional[str]
            Filter allowances valid on this date (YYYY-MM-DD)

        request_options : typing.Optional[RequestOptions]
            Request-specific configuration.

        Returns
        -------
        CatalogAllowanceListResponse
            Successful response with paginated allowance list

        Examples
        --------
        import asyncio

        from ottimate import AsyncOttimate

        client = AsyncOttimate(
            api_key="YOUR_API_KEY",
            api_version="YOUR_API_VERSION",
            api_key="YOUR_API_KEY",
        )


        async def main() -> None:
            await client.catalog.get_catalog_allowances(
                ottimate_company_id=15196,
            )


        asyncio.run(main())
        """
        _response = await self._raw_client.get_catalog_allowances(
            ottimate_company_id=ottimate_company_id,
            catalog_entry_id=catalog_entry_id,
            ottimate_location_id=ottimate_location_id,
            effective_date=effective_date,
            request_options=request_options,
        )
        return _response.data

    async def post_catalog_allowances(
        self,
        *,
        ottimate_company_id: int,
        catalog_entry_id: int,
        amount: float,
        start_date: dt.datetime,
        allowance_type: typing.Optional[PostCatalogAllowancesRequestAllowanceType] = OMIT,
        unit_allowance: typing.Optional[float] = OMIT,
        target: typing.Optional[int] = OMIT,
        ottimate_location_id: typing.Optional[int] = OMIT,
        ottimate_group_id: typing.Optional[int] = OMIT,
        end_date: typing.Optional[dt.datetime] = OMIT,
        reference_id: typing.Optional[str] = OMIT,
        request_options: typing.Optional[RequestOptions] = None,
    ) -> CatalogAllowanceObject:
        """
        Creates a new catalog allowance for a catalog entry.

        Parameters
        ----------
        ottimate_company_id : int
            The Ottimate company ID this allowance belongs to

        catalog_entry_id : int
            The ID of the catalog entry this allowance applies to

        amount : float
            Allowance amount for the pack size

        start_date : dt.datetime
            Allowance effective start date (ISO 8601)

        allowance_type : typing.Optional[PostCatalogAllowancesRequestAllowanceType]
            Type of allowance (default: 'allowance')

        unit_allowance : typing.Optional[float]
            Allowance amount per individual unit (default: 0)

        target : typing.Optional[int]
            Target level: 10=company (default), 15=location_group, 20=location

        ottimate_location_id : typing.Optional[int]
            Location ID (required when target is 20/location)

        ottimate_group_id : typing.Optional[int]
            Location group ID (required when target is 15/location_group)

        end_date : typing.Optional[dt.datetime]
            Allowance effective end date (ISO 8601, optional)

        reference_id : typing.Optional[str]
            Unique reference ID for upsert operations

        request_options : typing.Optional[RequestOptions]
            Request-specific configuration.

        Returns
        -------
        CatalogAllowanceObject
            Catalog allowance created successfully

        Examples
        --------
        import asyncio
        import datetime

        from ottimate import AsyncOttimate

        client = AsyncOttimate(
            api_key="YOUR_API_KEY",
            api_version="YOUR_API_VERSION",
            api_key="YOUR_API_KEY",
        )


        async def main() -> None:
            await client.catalog.post_catalog_allowances(
                catalog_entry_id=12347,
                ottimate_company_id=15196,
                allowance_type="rebate",
                amount=2.0,
                start_date=datetime.datetime.fromisoformat(
                    "2024-12-01 00:00:00+00:00",
                ),
                reference_id="allow-new",
            )


        asyncio.run(main())
        """
        _response = await self._raw_client.post_catalog_allowances(
            ottimate_company_id=ottimate_company_id,
            catalog_entry_id=catalog_entry_id,
            amount=amount,
            start_date=start_date,
            allowance_type=allowance_type,
            unit_allowance=unit_allowance,
            target=target,
            ottimate_location_id=ottimate_location_id,
            ottimate_group_id=ottimate_group_id,
            end_date=end_date,
            reference_id=reference_id,
            request_options=request_options,
        )
        return _response.data

    async def patch_catalog_allowances_id(
        self,
        id: str,
        *,
        allowance_type: typing.Optional[PatchCatalogAllowancesIdRequestAllowanceType] = OMIT,
        amount: typing.Optional[float] = OMIT,
        unit_allowance: typing.Optional[float] = OMIT,
        target: typing.Optional[int] = OMIT,
        ottimate_location_id: typing.Optional[int] = OMIT,
        ottimate_group_id: typing.Optional[int] = OMIT,
        start_date: typing.Optional[dt.datetime] = OMIT,
        end_date: typing.Optional[dt.datetime] = OMIT,
        request_options: typing.Optional[RequestOptions] = None,
    ) -> CatalogAllowanceObject:
        """
        Updates an existing catalog allowance by ID.

        Parameters
        ----------
        id : str
            The unique ID of the catalog allowance

        allowance_type : typing.Optional[PatchCatalogAllowancesIdRequestAllowanceType]
            Type of allowance

        amount : typing.Optional[float]
            Allowance amount

        unit_allowance : typing.Optional[float]
            Unit allowance amount

        target : typing.Optional[int]
            Target (10=company, 15=location_group, 20=location)

        ottimate_location_id : typing.Optional[int]
            Location ID

        ottimate_group_id : typing.Optional[int]
            Restaurant group ID

        start_date : typing.Optional[dt.datetime]
            Start date

        end_date : typing.Optional[dt.datetime]
            End date

        request_options : typing.Optional[RequestOptions]
            Request-specific configuration.

        Returns
        -------
        CatalogAllowanceObject
            Catalog allowance updated successfully

        Examples
        --------
        import asyncio
        import datetime

        from ottimate import AsyncOttimate

        client = AsyncOttimate(
            api_key="YOUR_API_KEY",
            api_version="YOUR_API_VERSION",
            api_key="YOUR_API_KEY",
        )


        async def main() -> None:
            await client.catalog.patch_catalog_allowances_id(
                id="racc_90832490382904802384",
                amount=2.5,
                end_date=datetime.datetime.fromisoformat(
                    "2025-12-31 23:59:59+00:00",
                ),
            )


        asyncio.run(main())
        """
        _response = await self._raw_client.patch_catalog_allowances_id(
            id,
            allowance_type=allowance_type,
            amount=amount,
            unit_allowance=unit_allowance,
            target=target,
            ottimate_location_id=ottimate_location_id,
            ottimate_group_id=ottimate_group_id,
            start_date=start_date,
            end_date=end_date,
            request_options=request_options,
        )
        return _response.data

    async def post_catalog_allowances_bulk(
        self,
        *,
        ottimate_company_id: int,
        allowances: typing.Sequence[CatalogAllowanceBulkCreateRequestAllowancesItem],
        request_options: typing.Optional[RequestOptions] = None,
    ) -> CatalogBulkResponse:
        """
        Creates or updates multiple catalog allowances in a single request. Uses upsert logic based on reference_id. Maximum 1000 records per request.

        Parameters
        ----------
        ottimate_company_id : int
            Company ID

        allowances : typing.Sequence[CatalogAllowanceBulkCreateRequestAllowancesItem]
            List of allowances to create/update (max 1000)

        request_options : typing.Optional[RequestOptions]
            Request-specific configuration.

        Returns
        -------
        CatalogBulkResponse
            All allowances created/updated successfully

        Examples
        --------
        import asyncio
        import datetime

        from ottimate import AsyncOttimate
        from ottimate.catalog import CatalogAllowanceBulkCreateRequestAllowancesItem

        client = AsyncOttimate(
            api_key="YOUR_API_KEY",
            api_version="YOUR_API_VERSION",
            api_key="YOUR_API_KEY",
        )


        async def main() -> None:
            await client.catalog.post_catalog_allowances_bulk(
                ottimate_company_id=15196,
                allowances=[
                    CatalogAllowanceBulkCreateRequestAllowancesItem(
                        catalog_entry_id="cte_12348",
                        allowance_type="rebate",
                        amount=1.5,
                        start_date=datetime.datetime.fromisoformat(
                            "2024-12-01 00:00:00+00:00",
                        ),
                        reference_id="allow-bulk",
                    ),
                    CatalogAllowanceBulkCreateRequestAllowancesItem(
                        catalog_entry_id="cte_12349",
                        allowance_type="discount",
                        amount=2.0,
                        start_date=datetime.datetime.fromisoformat(
                            "2024-12-01 00:00:00+00:00",
                        ),
                        reference_id="allow-bulk",
                    ),
                ],
            )


        asyncio.run(main())
        """
        _response = await self._raw_client.post_catalog_allowances_bulk(
            ottimate_company_id=ottimate_company_id, allowances=allowances, request_options=request_options
        )
        return _response.data

    async def get_catalog_prices(
        self,
        *,
        ottimate_company_id: int,
        catalog_entry_id: typing.Optional[str] = None,
        ottimate_location_id: typing.Optional[int] = None,
        effective_date: typing.Optional[str] = None,
        price_type: typing.Optional[GetCatalogPricesRequestPriceType] = None,
        request_options: typing.Optional[RequestOptions] = None,
    ) -> GetCatalogPricesResponse:
        """
        Returns a paginated list of catalog prices, filterable by company, catalog entry, location, effective date, and price type.

        Parameters
        ----------
        ottimate_company_id : int
            Filter prices by company ID (required)

        catalog_entry_id : typing.Optional[str]
            Filter by catalog entry ID

        ottimate_location_id : typing.Optional[int]
            Filter by location ID

        effective_date : typing.Optional[str]
            Filter prices valid on this date (YYYY-MM-DD)

        price_type : typing.Optional[GetCatalogPricesRequestPriceType]
            Filter by price type

        request_options : typing.Optional[RequestOptions]
            Request-specific configuration.

        Returns
        -------
        GetCatalogPricesResponse
            Successful response with paginated price list

        Examples
        --------
        import asyncio

        from ottimate import AsyncOttimate

        client = AsyncOttimate(
            api_key="YOUR_API_KEY",
            api_version="YOUR_API_VERSION",
            api_key="YOUR_API_KEY",
        )


        async def main() -> None:
            await client.catalog.get_catalog_prices(
                ottimate_company_id=15196,
            )


        asyncio.run(main())
        """
        _response = await self._raw_client.get_catalog_prices(
            ottimate_company_id=ottimate_company_id,
            catalog_entry_id=catalog_entry_id,
            ottimate_location_id=ottimate_location_id,
            effective_date=effective_date,
            price_type=price_type,
            request_options=request_options,
        )
        return _response.data

    async def post_catalog_prices(
        self,
        *,
        ottimate_company_id: int,
        catalog_entry_id: str,
        price: float,
        start_date: dt.datetime,
        unit_price: typing.Optional[float] = OMIT,
        price_type: typing.Optional[PostCatalogPricesRequestPriceType] = OMIT,
        ottimate_location_id: typing.Optional[int] = OMIT,
        ottimate_group_id: typing.Optional[int] = OMIT,
        end_date: typing.Optional[dt.datetime] = OMIT,
        reference_id: typing.Optional[str] = OMIT,
        request_options: typing.Optional[RequestOptions] = None,
    ) -> CatalogPriceItem:
        """
        Creates a new catalog price for a catalog entry.

        Parameters
        ----------
        ottimate_company_id : int
            The Ottimate company ID this price belongs to

        catalog_entry_id : str
            The ID of the catalog entry this price applies to

        price : float
            Price amount for the pack size

        start_date : dt.datetime
            Price effective start date (ISO 8601)

        unit_price : typing.Optional[float]
            Price amount per individual unit (optional)

        price_type : typing.Optional[PostCatalogPricesRequestPriceType]
            Price type: company (default), location_group, or location

        ottimate_location_id : typing.Optional[int]
            Location ID (required when price_type is 'location')

        ottimate_group_id : typing.Optional[int]
            Location group ID (required when price_type is 'location_group')

        end_date : typing.Optional[dt.datetime]
            Price effective end date (ISO 8601, optional)

        reference_id : typing.Optional[str]
            External reference ID for the price

        request_options : typing.Optional[RequestOptions]
            Request-specific configuration.

        Returns
        -------
        CatalogPriceItem
            Price created successfully

        Examples
        --------
        import asyncio
        import datetime

        from ottimate import AsyncOttimate

        client = AsyncOttimate(
            api_key="YOUR_API_KEY",
            api_version="YOUR_API_VERSION",
            api_key="YOUR_API_KEY",
        )


        async def main() -> None:
            await client.catalog.post_catalog_prices(
                ottimate_company_id=15196,
                catalog_entry_id="example_catalog_entry_id",
                price=100.0,
                start_date=datetime.datetime.fromisoformat(
                    "2024-12-05 10:30:00+00:00",
                ),
            )


        asyncio.run(main())
        """
        _response = await self._raw_client.post_catalog_prices(
            ottimate_company_id=ottimate_company_id,
            catalog_entry_id=catalog_entry_id,
            price=price,
            start_date=start_date,
            unit_price=unit_price,
            price_type=price_type,
            ottimate_location_id=ottimate_location_id,
            ottimate_group_id=ottimate_group_id,
            end_date=end_date,
            reference_id=reference_id,
            request_options=request_options,
        )
        return _response.data

    async def post_catalog_prices_bulk(
        self,
        *,
        ottimate_company_id: int,
        prices: typing.Sequence[CatalogPricesBulkCreateRequestPricesItem],
        request_options: typing.Optional[RequestOptions] = None,
    ) -> PostCatalogPricesBulkResponse:
        """
        Creates multiple catalog prices in a single request. Unlike allowances, this endpoint does not support upsert - duplicate reference_ids will be rejected. Maximum 1000 records per request.

        Parameters
        ----------
        ottimate_company_id : int
            Company ID

        prices : typing.Sequence[CatalogPricesBulkCreateRequestPricesItem]
            List of prices to create (max 1000)

        request_options : typing.Optional[RequestOptions]
            Request-specific configuration.

        Returns
        -------
        PostCatalogPricesBulkResponse
            All prices created successfully

        Examples
        --------
        import asyncio
        import datetime

        from ottimate import AsyncOttimate
        from ottimate.catalog import CatalogPricesBulkCreateRequestPricesItem

        client = AsyncOttimate(
            api_key="YOUR_API_KEY",
            api_version="YOUR_API_VERSION",
            api_key="YOUR_API_KEY",
        )


        async def main() -> None:
            await client.catalog.post_catalog_prices_bulk(
                ottimate_company_id=15196,
                prices=[
                    CatalogPricesBulkCreateRequestPricesItem(
                        catalog_entry_id="cte_12345",
                        price=25.99,
                        unit_price=2.16,
                        price_type="company",
                        start_date=datetime.datetime.fromisoformat(
                            "2024-01-01 00:00:00+00:00",
                        ),
                        reference_id="price-sysco-tom-1",
                    )
                ],
            )


        asyncio.run(main())
        """
        _response = await self._raw_client.post_catalog_prices_bulk(
            ottimate_company_id=ottimate_company_id, prices=prices, request_options=request_options
        )
        return _response.data

    async def get_catalog_variances(
        self,
        *,
        ottimate_company_id: int,
        variance_only: typing.Optional[bool] = None,
        expand: typing.Optional[str] = None,
        invoice_start_date: typing.Optional[str] = None,
        invoice_end_date: typing.Optional[str] = None,
        erp_vendor_ids: typing.Optional[str] = None,
        ottimate_location_ids: typing.Optional[str] = None,
        exception_type: typing.Optional[GetCatalogVariancesRequestExceptionType] = None,
        page: typing.Optional[int] = None,
        limit: typing.Optional[int] = None,
        request_options: typing.Optional[RequestOptions] = None,
    ) -> VarianceListResponse:
        """
        Returns catalog entries with price variance information from invoice items. By default, returns all items that had purchases in the specified period, including items with zero variance. Use variance_only=true to return only entries with non-zero variance. Use expand=invoice_items to include all invoice items inline (otherwise only the most recent invoice item is returned).

        Parameters
        ----------
        ottimate_company_id : int
            Filter by company ID (required)

        variance_only : typing.Optional[bool]
            If true, returns only entries with non-zero variance. Default: false (returns all items including zero variance)

        expand : typing.Optional[str]
            Comma-separated list of fields to expand. Supports 'invoice_items' to include all invoice items for each entry.

        invoice_start_date : typing.Optional[str]
            Start of date range (YYYY-MM-DD)

        invoice_end_date : typing.Optional[str]
            End of date range (YYYY-MM-DD)

        erp_vendor_ids : typing.Optional[str]
            Comma-separated ERP vendor IDs (accounting vendor IDs) to filter variances by vendor.

        ottimate_location_ids : typing.Optional[str]
            Comma-separated location IDs

        exception_type : typing.Optional[GetCatalogVariancesRequestExceptionType]
            Filter by variance type

        page : typing.Optional[int]
            Page number for pagination

        limit : typing.Optional[int]
            Number of items per page

        request_options : typing.Optional[RequestOptions]
            Request-specific configuration.

        Returns
        -------
        VarianceListResponse
            Successful response with paginated variance list

        Examples
        --------
        import asyncio

        from ottimate import AsyncOttimate

        client = AsyncOttimate(
            api_key="YOUR_API_KEY",
            api_version="YOUR_API_VERSION",
            api_key="YOUR_API_KEY",
        )


        async def main() -> None:
            await client.catalog.get_catalog_variances(
                ottimate_company_id=15196,
                variance_only=False,
                page=1,
                limit=20,
            )


        asyncio.run(main())
        """
        _response = await self._raw_client.get_catalog_variances(
            ottimate_company_id=ottimate_company_id,
            variance_only=variance_only,
            expand=expand,
            invoice_start_date=invoice_start_date,
            invoice_end_date=invoice_end_date,
            erp_vendor_ids=erp_vendor_ids,
            ottimate_location_ids=ottimate_location_ids,
            exception_type=exception_type,
            page=page,
            limit=limit,
            request_options=request_options,
        )
        return _response.data

    async def get_catalog_variances_id_invoice_items(
        self,
        id: str,
        *,
        page: typing.Optional[int] = None,
        limit: typing.Optional[int] = None,
        request_options: typing.Optional[RequestOptions] = None,
    ) -> InvoiceItemVarianceListResponse:
        """
        Returns paginated invoice items with price variances for a specific catalog entry. Use this endpoint when the expand=invoice_items response indicates has_more=true.

        Parameters
        ----------
        id : str
            The catalog entry ID

        page : typing.Optional[int]
            Page number

        limit : typing.Optional[int]
            Items per page (default: 20)

        request_options : typing.Optional[RequestOptions]
            Request-specific configuration.

        Returns
        -------
        InvoiceItemVarianceListResponse
            Successful response with paginated invoice items

        Examples
        --------
        import asyncio

        from ottimate import AsyncOttimate

        client = AsyncOttimate(
            api_key="YOUR_API_KEY",
            api_version="YOUR_API_VERSION",
            api_key="YOUR_API_KEY",
        )


        async def main() -> None:
            await client.catalog.get_catalog_variances_id_invoice_items(
                id="racc_90832490382904802384",
                page=1,
                limit=20,
            )


        asyncio.run(main())
        """
        _response = await self._raw_client.get_catalog_variances_id_invoice_items(
            id, page=page, limit=limit, request_options=request_options
        )
        return _response.data
