Download the PHP package iperson1337/nct-client-bundle without Composer
On this page you can find all versions of the php package iperson1337/nct-client-bundle. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.
Download iperson1337/nct-client-bundle
More information about iperson1337/nct-client-bundle
Files in iperson1337/nct-client-bundle
Package nct-client-bundle
Short Description Symfony client for the National Catalog of Goods of Kazakhstan (НКТ): nationalcatalog.kz portal API (product requests, attributes, dictionaries) and nct.gov.kz public lookups, with retries, rate limiting and typed errors
License MIT
Homepage https://github.com/iperson1337/nct-client-bundle
Informations about the package nct-client-bundle
NCT Client Bundle
English · Қазақша
Symfony client for the National Catalog of Goods of Kazakhstan (НКТ): the complete nationalcatalog.kz portal API (product requests and their lifecycle, files, duplicates, cards, attributes, dictionaries) and the public nct.gov.kz API (search, timestamp export). Ships with retries, Redis-backed rate limiting, typed exceptions and value objects for the protocol's data.
The bundle knows the portal protocol and nothing about your domain model: responses come back as decoded arrays (or small value objects), and what to do with them is up to the application.
Coverage: all 23 operations of the official portal specification (https://nationalcatalog.kz/gwp/docs)
and all 5 requests of the public nct.gov.kz collection.
Features
- Portal API (
nationalcatalog.kz,X-API-KEY): create, update, delete, cancel, moderate and publish product requests; create an edit request for a published card by NTIN; list requests with a status filter; read status and details; resolve duplicates; upload photos and PDF documents; look cards up by NTIN/GTIN (single and list); base and extended-card attribute definitions; dictionaries (list, roots, children, paged items). - Public API (
nct.gov.kz): search cards by one code, by a batch of up to 100 codes, or by name; fetch a card by id; JWT-protected export of changed products by timestamp with a count. - One façade —
NctClient::fetchProductByGtin()picks the configured lookup source. - Rate limiting — a proactive sliding-window limiter per host (Redis, Lua, shared between
processes) plus reactive retries after
429/422 TOO_MANY_REQUESTS. - Per-call overrides — short timeout and single attempt for synchronous UI checks.
- Typed exceptions — all extend
\RuntimeExceptionand implementNctExceptionInterface. - Value objects —
AttributeDefinition,DictionaryItem,ProductRequestPayload(with order-insensitive comparison),RequestStatus/ExtendedRequestStatus/ImageType/DuplicateDecisionenums,RequestDetailsParser,NationalCatalogIdentifier.
Requirements
- PHP 8.3+
- Symfony 7.4 (
config,dependency-injection,http-kernel,cache) - Guzzle 7
- Optional: the
redisextension orpredis/predisfor the Redis rate limiter
Installation
Without Symfony Flex, register the bundle:
Configuration
config/packages/iperson1337_nct_client.yaml:
All keys are optional; the values above are the defaults (except api_key, which is empty, and
redis_dsn, which is null).
| Key | Meaning |
|---|---|
portal.host, portal.api_key |
portal address and the applicant's API key; every request operation goes here regardless of lookup_source |
portal.timeout, portal.connect_timeout |
default Guzzle timeouts for both hosts; overridable per call with $timeoutOverride |
portal.retry_count |
how many times to retry after 429 / 422 TOO_MANY_REQUESTS; $maxAttempts = 1 on a call disables retries |
*.retry_after_seconds |
sleep() before a retry, per host |
*.rate_limit |
proactive limit checked before each request: a Redis sliding window with its own key per host, so the hosts never throttle each other |
lookup_source |
where NctClient::fetchProductByGtin() looks: nctgov — public search, normalised to {gtin, ntin_code}; nationalcatalog — GET /products/{gtin} on the portal, raw response |
redis_dsn |
Redis for the limiters; when Redis is unavailable the limit is skipped with a warning and the request proceeds |
Registered services (autowire by class): NctClient, NctPortalClient, NctGovClient,
NctHttpClient, RequestDetailsParser, NationalCatalogIdentifier.
Usage
Product by GTIN
A short timeout and a single attempt for a synchronous check in a UI action (don't wait the full
timeout and don't sleep on 429):
GTIN is not unique on the portal — a GTIN may belong to several cards. To see all of them:
Product request lifecycle
The portal rejects an NTIN attribute in a create request. To edit a published card either
pass ntin in the payload of submitProductRequest() or start from the card itself:
name_ru, name_kk, ntin and tnved cannot be changed on an edit request; extended cards (with a
KZTIN) are not editable through the API.
ProductRequestPayload compares bodies regardless of key and attribute order — use it to decide
whether a stored request actually changed before sending a PUT that would re-trigger moderation:
Request status and details
Base statuses (RequestStatus): new, onModeration, accepted, underRevision, cancelled,
readyToPublish, existingProductSelected, completed, rejected. Extended-card statuses in
gzStatus (ExtendedRequestStatus): acceptedGz, underRevisionGz, rejectedGz, publishingGz.
Duplicates
When moderation sends a request back for DUPLICATE_REVIEW:
Photos and documents
Files are uploaded to the request first (statuses new, underRevision, cancelled; 5 MB max),
then referenced by filename in the payload:
An extended card (isGov: true) needs front and back photos, the brand attribute and at least
five additional characteristics before it can go to moderation.
Attribute definitions and dictionaries
nct.gov.kz: search, card, export
Timestamp export needs an nct.gov.kz account (limit: 120 requests per minute):
After several failed logins the portal locks the account for 10 minutes (403, locked_seconds).
NTIN / XTIN format
Identifiers are strings: leading zeros matter.
Exceptions
Every exception extends \RuntimeException and implements NctExceptionInterface, so an existing
catch (\RuntimeException) keeps working and catch (NctExceptionInterface) catches any NCT failure.
| Exception | When |
|---|---|
NctProductNotFoundException |
no card for the GTIN (nct.gov.kz answered 404 "product … not found" or an empty list). The message is stable: Product with GTIN {gtin} not found in NCT (nct.gov.kz) |
NctRateLimitExceededException |
429 or 422 TOO_MANY_REQUESTS and no attempts left (or maxAttempts: 1) |
NctRequestTimeoutException |
connection timeout (ConnectException) or response timeout (cURL 28) |
NctTransportException |
5xx, network failure, attempts exhausted |
NctClientException |
any other 4xx: invalid request, forbidden, changed API path. The message carries HTTP {status} {body} — the full body (capped at 2000 characters, marked when cut), because the body is what explains which attribute was rejected |
NctResponseException |
2xx whose body is not valid JSON |
A 404 with a {"Message": "product … not found"} body counts as "not found" only for requests
that opted in (NctRequest::$notFoundAsEmpty): a 404 caused by a changed API path must not look
like a missing product.
Rate limiting and retries
Two layers protect every request:
- Proactive limit —
RateLimiterInterface::waitIfNeeded()before the request, one limiter per host (NctTarget::Portal/NctTarget::Gov). The Redis implementation keeps a sorted set of timestamps; an atomic Lua script counts requests in the window and, when full, returns the time until the oldest one leaves it. If Redis fails, the limit is skipped with a warning — a limiter must never stop the host process. - Retry after 429 — up to
retry_countretries with the host'sretry_after_secondspause;maxAttempts: 1on a call turns retries off and surfacesNctRateLimitExceededExceptionat once.
One Guzzle client serves both hosts. Portal requests carry Content-Type: application/json,
Accept and X-API-KEY (multipart uploads let Guzzle set the boundary); nct.gov.kz requests carry
Accept only, plus Authorization: JWT … for the export — the API key never leaves for the public host.
Endpoints
| Method | HTTP |
|---|---|
NctPortalClient::submitProductRequest() |
POST /gwp/portal/api/v1/products/requests |
NctPortalClient::createEditRequest() |
POST /gwp/portal/api/v1/products/requests/{ntin}/edit-product |
NctPortalClient::updateProductRequest() |
PUT /gwp/portal/api/v1/products/requests/{id} |
NctPortalClient::deleteProductRequest() |
DELETE /gwp/portal/api/v1/products/requests/{id} |
NctPortalClient::submitToModeration() |
PUT /gwp/portal/api/v1/products/requests/{id}/moderation |
NctPortalClient::cancelRequest() |
PUT /gwp/portal/api/v1/products/requests/{id}/cancel |
NctPortalClient::publishRequest() |
PUT /gwp/portal/api/v1/products/requests/{id}/publish |
NctPortalClient::fetchRequestStatus() |
GET /gwp/portal/api/v1/products/requests/{id}/status |
NctPortalClient::fetchRequestDetails() |
GET /gwp/portal/api/v1/products/requests/{id}/details |
NctPortalClient::fetchProductRequests() |
GET /gwp/portal/api/v1/products/requests?page=&size=[&status=] |
NctPortalClient::fetchDuplicates() |
GET /gwp/portal/api/v1/products/requests/{id}/duplicates |
NctPortalClient::decideDuplicates() |
POST /gwp/portal/api/v1/products/requests/{id}/duplicate-decision |
NctPortalClient::uploadPhoto() |
POST /gwp/portal/api/v1/storage/{requestId}/upload?imageType= |
NctPortalClient::uploadDocument() |
POST /gwp/portal/api/v1/storage/{requestId}/upload/document |
NctPortalClient::deleteFile() |
DELETE /gwp/portal/api/v1/storage/{requestId}/{fileName} |
NctPortalClient::fetchProductByGtin() |
GET /gwp/portal/api/v1/products/{tin} |
NctPortalClient::fetchProductsByTin() |
GET /gwp/portal/api/v2/products/{tin} |
NctPortalClient::fetchAttributes() |
GET /gwp/portal/api/v1/products/requests/attributes |
NctPortalClient::fetchGovAttributes() |
GET /gwp/portal/api/v1/products/requests/categories/{oktru}/gov-attributes |
NctPortalClient::fetchDictionaries() |
GET /gwp/portal/api/v1/dictionaries |
NctPortalClient::fetchDictionaryRoots() |
GET /gwp/portal/api/v1/dictionaries/{code}/roots |
NctPortalClient::fetchDictionaryChildren() |
GET /gwp/portal/api/v1/dictionaries/{code}/children/{parentId} |
NctPortalClient::fetchDictionaryItems() |
GET /gwp/portal/api/v1/dictionaries/{code}/items?page=&size= |
NctGovClient::searchByGtin(), searchByTins() |
GET /api/integration/ofd/search_ofd/?tin={codes} |
NctGovClient::searchByName() |
GET /api/integration/ofd/search_ofd/?q={name}&limit=&offset= |
NctGovClient::fetchProduct() |
GET /api/core/products/{id} |
NctGovClient::login() |
POST /api/auth/login/ |
NctGovClient::exportProducts() |
GET /api/integration/ofd/ofd_product/?from=&limit=&offset= (JWT) |
NctGovClient::countChangedProducts() |
GET /api/integration/ofd/ofd_product_count/?from= (JWT) |
Testing
NctHttpClientTest is a snapshot test of the transport (31 scenarios: retries, 422, every kind
of 404, timeouts, error bodies, headers per host); the baseline lives in tests/Http/__snapshots__/.
To re-record it deliberately: UPDATE_SNAPSHOTS=1 vendor/bin/phpunit. NctPortalClientTest and
NctGovClientTest pin method, path, query, body and headers of every operation against a Guzzle
MockHandler.
NctClient, NctPortalClient and NctGovClient are not final, so they can be stubbed in the
consumer's own tests.
Scope
The bundle implements the portal protocol. It deliberately does not store requests or their statuses, decide when an identifier may be written to a product, build request payloads from a domain model, or map attribute and dictionary rows into the consumer's own reference data. The value objects give those rows a predictable shape; the mapping stays in the application.
License
MIT — see LICENSE.
All versions of nct-client-bundle with dependencies
guzzlehttp/guzzle Version ^7.0
psr/log Version ^3.0
symfony/cache Version ^7.4
symfony/config Version ^7.4
symfony/dependency-injection Version ^7.4
symfony/http-kernel Version ^7.4