This commit is contained in:
Clusy
2026-08-06 04:47:05 +00:00
parent 62bdd6291e
commit 2d4eee2950
81 changed files with 0 additions and 10766 deletions
-6
View File
@@ -1,6 +0,0 @@
PORT=3001
HOST=0.0.0.0
SANDBOX_URL=https://markirovka.sandbox.crptech.ru/api/v3/true-api
PROD_URL=https://markirovka.crpt.ru/api/v3/true-api
PUBLIC_CHECK_URL=https://mobile.api.crpt.ru/mobile/check
CORS_ORIGIN=*
-14
View File
@@ -1,14 +0,0 @@
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json tsconfig.json ./
RUN npm ci
COPY src/ ./src/
RUN npm run build
FROM node:20-alpine AS runner
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --production
COPY --from=builder /app/dist ./dist
EXPOSE 3001
CMD ["node", "dist/index.js"]
-2729
View File
File diff suppressed because it is too large Load Diff
-27
View File
@@ -1,27 +0,0 @@
{
"name": "chestny-znak-api-backend",
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "tsx watch src/index.ts",
"build": "tsc",
"start": "node dist/index.js",
"lint": "tsc --noEmit",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@fastify/cors": "^11.0.1",
"@fastify/multipart": "^9.0.3",
"fastify": "^5.3.1",
"got": "^14.4.6",
"node-cache": "^5.1.2",
"xml2js": "^0.6.2"
},
"devDependencies": {
"@types/node": "^22.13.1",
"@types/xml2js": "^0.4.14",
"eslint": "^9.19.0",
"tsx": "^4.19.3",
"typescript": "^5.7.3"
}
}
-29
View File
@@ -1,29 +0,0 @@
export type TrueApiEnv = 'sandbox' | 'prod'
export const envUrls: Record<TrueApiEnv, { baseUrl: string; publicCheckUrl: string }> = {
sandbox: {
baseUrl: process.env.SANDBOX_URL || 'https://markirovka.sandbox.crptech.ru/api/v3/true-api',
publicCheckUrl: process.env.SANDBOX_PUBLIC_CHECK_URL || 'https://mobile.api.crpt.ru/mobile/check',
},
prod: {
baseUrl: process.env.PROD_URL || 'https://markirovka.crpt.ru/api/v3/true-api',
publicCheckUrl: process.env.PROD_PUBLIC_CHECK_URL || 'https://mobile.api.crpt.ru/mobile/check',
},
}
export const config = {
port: parseInt(process.env.PORT || '3001', 10),
host: process.env.HOST || '0.0.0.0',
corsOrigin: process.env.CORS_ORIGIN || '*',
trueApi: {
authPath: '/auth',
cisesInfoPath: 'cises/info',
codesCheckPath: 'codes/check',
batchSize: 1000,
tokenTTL: 10 * 60 * 60 * 1000,
maxRetries: 3,
publicCheckConcurrency: 10,
},
}
-34
View File
@@ -1,34 +0,0 @@
import Fastify from 'fastify'
import cors from '@fastify/cors'
import multipart from '@fastify/multipart'
import { config } from './config.js'
import { authRoutes } from './routes/auth.js'
import { cisesRoutes } from './routes/cises.js'
import { updRoutes } from './routes/upd.js'
import { plansRoutes } from './routes/plans.js'
const app = Fastify({ logger: true })
await app.register(cors, { origin: config.corsOrigin })
await app.register(multipart, {
limits: {
fileSize: 10 * 1024 * 1024,
files: 1,
},
throwFileSizeLimit: true,
})
await app.register(authRoutes)
await app.register(cisesRoutes)
await app.register(updRoutes)
await app.register(plansRoutes)
app.get('/health', async () => ({ status: 'ok', timestamp: new Date().toISOString() }))
try {
await app.listen({ port: config.port, host: config.host })
console.log(`Server running on http://${config.host}:${config.port}`)
} catch (err) {
app.log.error(err)
process.exit(1)
}
-86
View File
@@ -1,86 +0,0 @@
import type { FastifyInstance } from 'fastify'
import type { TrueApiEnv } from '../config.js'
import { getAuthKey, signIn, clearToken, getAuthStatus, setTokenDirect } from '../services/auth-service.js'
import { AppError, extractDebugInfo } from '../services/cises-service.js'
const VALID_ENVS = ['sandbox', 'prod']
function parseEnv(query: Record<string, string>): TrueApiEnv {
const env = query.env
if (env && VALID_ENVS.includes(env)) return env as TrueApiEnv
return 'sandbox'
}
const signInSchema = {
body: {
type: 'object',
required: ['uuid', 'data'],
properties: {
uuid: { type: 'string' },
data: { type: 'string' },
inn: { type: 'string' },
},
},
}
export async function authRoutes(app: FastifyInstance): Promise<void> {
app.get('/auth/key', async (request, reply) => {
const env = parseEnv(request.query as Record<string, string>)
try {
const authKey = await getAuthKey(env)
return { uuid: authKey.uuid, data: authKey.data }
} catch (err) {
if (err instanceof AppError) {
return reply.status(err.statusCode).send({
error: err.message,
debugInfo: err.debugInfo,
})
}
const debugInfo = extractDebugInfo(err)
return reply.status(502).send({
error: 'Failed to get auth key from True API',
debugInfo,
})
}
})
app.post<{ Body: { uuid: string; data: string; inn?: string } }>('/auth/simpleSignIn', { schema: signInSchema }, async (request, reply) => {
const env = parseEnv(request.query as Record<string, string>)
try {
const { uuid, data, inn } = request.body
const result = await signIn(env, { uuid, data, inn })
request.log.info({ env, token: result.token }, 'auth: token received')
return result
} catch (err) {
if (err instanceof AppError) {
return reply.status(err.statusCode).send({
error: err.message,
debugInfo: err.debugInfo,
})
}
const debugInfo = extractDebugInfo(err)
return reply.status(502).send({
error: 'Failed to sign in to True API',
debugInfo,
})
}
})
app.get('/auth/status', async (request) => {
const env = parseEnv(request.query as Record<string, string>)
return getAuthStatus(env)
})
app.post('/auth/logout', async (request) => {
const env = parseEnv(request.query as Record<string, string>)
clearToken(env)
return { success: true }
})
app.post<{ Body: { token: string } }>('/auth/setToken', async (request) => {
const env = parseEnv(request.query as Record<string, string>)
setTokenDirect(env, request.body.token)
request.log.info({ env, tokenPrefix: request.body.token.slice(0, 20) + '...' }, 'auth: token set manually')
return { success: true }
})
}
-49
View File
@@ -1,49 +0,0 @@
import type { FastifyInstance } from 'fastify'
import type { TrueApiEnv } from '../config.js'
import { checkCodesPublic, checkCodesAuth, AppError } from '../services/cises-service.js'
import type { CheckCodesRequest } from '../types/index.js'
const VALID_ENVS = ['sandbox', 'prod']
function parseEnv(query: Record<string, string>): TrueApiEnv {
const env = query.env
if (env && VALID_ENVS.includes(env)) return env as TrueApiEnv
return 'sandbox'
}
const checkCodesSchema = {
body: {
type: 'object',
required: ['codes'],
properties: {
codes: {
type: 'array',
items: { type: 'string', minLength: 1, maxLength: 256 },
minItems: 1,
maxItems: 10000,
},
},
},
}
export async function cisesRoutes(app: FastifyInstance): Promise<void> {
app.post<{ Body: CheckCodesRequest }>('/api/check-codes/public', { schema: checkCodesSchema }, async (request) => {
const env = parseEnv(request.query as Record<string, string>)
return checkCodesPublic(env, request.body)
})
app.post<{ Body: CheckCodesRequest }>('/api/check-codes/auth', { schema: checkCodesSchema }, async (request, reply) => {
const env = parseEnv(request.query as Record<string, string>)
try {
return await checkCodesAuth(env, request.body, (msg, data) => request.log.error(data, msg))
} catch (err) {
if (err instanceof AppError) {
return reply.status(err.statusCode).send({
error: err.message,
debugInfo: err.debugInfo,
})
}
throw err
}
})
}
-52
View File
@@ -1,52 +0,0 @@
import { readdir, readFile, stat } from 'node:fs/promises'
import { resolve, extname } from 'node:path'
import { fileURLToPath } from 'node:url'
import type { FastifyInstance } from 'fastify'
const __filename = fileURLToPath(import.meta.url)
const __dirname = resolve(__filename, '..')
const PLANS_DIR = resolve(__dirname, '../../plans')
export async function plansRoutes(app: FastifyInstance): Promise<void> {
app.get('/api/plans', async () => {
let files: string[]
try {
files = await readdir(PLANS_DIR)
} catch {
return []
}
const mdFiles: { name: string; path: string; size: number }[] = []
for (const file of files) {
if (extname(file) !== '.md') continue
const fullPath = resolve(PLANS_DIR, file)
try {
const stats = await stat(fullPath)
mdFiles.push({ name: file, path: file, size: stats.size })
} catch {
// skip files that can't be stat'd
}
}
mdFiles.sort((a, b) => b.name.localeCompare(a.name))
return mdFiles
})
app.get<{ Params: { filename: string } }>('/api/plans/:filename', async (request, reply) => {
const { filename } = request.params
if (extname(filename) !== '.md' || filename.includes('..') || filename.includes('/') || filename.includes('\\')) {
return reply.status(400).send({ error: 'Invalid filename' })
}
const fullPath = resolve(PLANS_DIR, filename)
try {
const content = await readFile(fullPath, 'utf-8')
return { name: filename, content }
} catch {
return reply.status(404).send({ error: 'File not found' })
}
})
}
-61
View File
@@ -1,61 +0,0 @@
import type { FastifyInstance } from 'fastify'
import type { TrueApiEnv } from '../config.js'
import { parseUpdXml } from '../services/upd-parser.js'
import { checkCodesAuth, AppError } from '../services/cises-service.js'
const VALID_ENVS = ['sandbox', 'prod']
function parseEnv(query: Record<string, string>): TrueApiEnv {
const env = query.env
if (env && VALID_ENVS.includes(env)) return env as TrueApiEnv
return 'sandbox'
}
export async function updRoutes(app: FastifyInstance): Promise<void> {
app.post('/api/upload-upd', async (request, reply) => {
const env = parseEnv(request.query as Record<string, string>)
const data = await request.file()
if (!data) {
return reply.status(400).send({ error: 'No file uploaded' })
}
const buffer = await data.toBuffer()
const xmlContent = buffer.toString('utf-8')
const fileName = data.filename
let parsed
try {
parsed = await parseUpdXml(xmlContent, fileName)
} catch {
return reply.status(400).send({ error: 'Invalid XML file' })
}
if (parsed.codes.length === 0) {
return reply.status(400).send({ error: 'No marking codes found in UPD' })
}
try {
const checkResults = await checkCodesAuth(env, { codes: parsed.codes })
return {
document: {
number: parsed.documentNumber,
date: parsed.documentDate,
seller: parsed.sellerName,
buyer: parsed.buyerName,
fileName: parsed.fileName,
},
codesFound: parsed.codes.length,
results: checkResults,
}
} catch (err) {
if (err instanceof AppError) {
return reply.status(err.statusCode).send({
error: err.message,
debugInfo: err.debugInfo,
})
}
throw err
}
})
}
-99
View File
@@ -1,99 +0,0 @@
import got from 'got'
import type { Response } from 'got'
import { config, envUrls } from '../config.js'
import type { TrueApiEnv } from '../config.js'
import { getToken, setToken, invalidateToken, hasToken, getTokenTTL } from './token-cache.js'
import { AppError, extractDebugInfo } from './cises-service.js'
import type { AuthKeyResponse, AuthSignInRequest, AuthSignInResponse } from '../types/index.js'
function loggedClient(prefixUrl: string) {
return got.extend({
prefixUrl,
responseType: 'json',
hooks: {
beforeRequest: [
(options) => {
const headers = { ...options.headers } as Record<string, string>
if (headers.authorization) {
headers.authorization = 'Bearer ***'
}
console.log(JSON.stringify({
type: 'trueapi_request',
method: options.method,
url: String(options.url || ''),
headers,
body: options.json || options.body || null,
}))
},
],
afterResponse: [
(response: Response) => {
console.log(JSON.stringify({
type: 'trueapi_response',
statusCode: response.statusCode,
url: response.requestUrl,
body: response.body,
}))
return response
},
],
},
})
}
function authClient(env: TrueApiEnv) {
return loggedClient(envUrls[env].baseUrl + config.trueApi.authPath)
}
export async function getAuthKey(env: TrueApiEnv): Promise<AuthKeyResponse> {
try {
const { body } = await authClient(env).get<AuthKeyResponse>('key')
return body
} catch (err) {
const info = extractDebugInfo(err)
const status = info?.response.statusCode || 502
throw new AppError(info?.response.body ? String(info.response.body) : 'Failed to get auth key', status, info)
}
}
export async function signIn(env: TrueApiEnv, body: AuthSignInRequest): Promise<AuthSignInResponse> {
try {
const requestBody: Record<string, unknown> = { uuid: body.uuid, data: body.data }
if (body.inn) {
requestBody.inn = body.inn
}
const { body: response } = await authClient(env).post<AuthSignInResponse>('simpleSignIn', {
json: requestBody,
})
setToken(env, response.token)
return response
} catch (err) {
const info = extractDebugInfo(err)
const status = info?.response.statusCode || 502
throw new AppError(info?.response.body ? String(info.response.body) : 'Failed to sign in', status, info)
}
}
export function getCachedToken(env: TrueApiEnv): string | undefined {
return getToken(env)
}
export function setTokenDirect(env: TrueApiEnv, token: string): void {
setToken(env, token)
}
export function clearToken(env: TrueApiEnv): void {
invalidateToken(env)
}
export function isAuthenticated(env: TrueApiEnv): boolean {
return hasToken(env)
}
export function getAuthStatus(env: TrueApiEnv): { authenticated: boolean; tokenExpiresAt: string | null } {
const ttl = getTokenTTL(env)
return {
authenticated: hasToken(env),
tokenExpiresAt: ttl ? new Date(ttl).toISOString() : null,
}
}
-293
View File
@@ -1,293 +0,0 @@
import got from 'got'
import type { Response } from 'got'
import { config, envUrls } from '../config.js'
import type { TrueApiEnv } from '../config.js'
import { getCachedToken, clearToken } from './auth-service.js'
import type {
CisInfoResponseItem,
PublicCheckResponse,
CheckCodesRequest,
CheckCodesResponse,
SingleCodeResult,
ApiDebugInfo,
} from '../types/index.js'
// Статусы КМ, при которых код считается действительным (в обороте)
const VALID_STATUSES = ['INTRODUCED', 'APPLIED', 'EMITTED']
function loggedClient(prefixUrl?: string) {
return got.extend({
...(prefixUrl ? { prefixUrl } : {}),
responseType: 'json',
hooks: {
beforeRequest: [
(options) => {
const headers = { ...options.headers } as Record<string, string>
if (headers.authorization) {
headers.authorization = 'Bearer ***'
}
console.log(JSON.stringify({
type: 'trueapi_request',
method: options.method,
url: String(options.url || ''),
headers,
body: options.json || options.body || null,
}))
},
],
afterResponse: [
(response: Response) => {
console.log(JSON.stringify({
type: 'trueapi_response',
statusCode: response.statusCode,
url: response.requestUrl,
body: response.body,
}))
return response
},
],
},
})
}
function apiClient(env: TrueApiEnv) {
return loggedClient(envUrls[env].baseUrl)
}
export async function checkCodesPublic(env: TrueApiEnv, body: CheckCodesRequest): Promise<CheckCodesResponse> {
const results: SingleCodeResult[] = []
const queue = [...body.codes]
async function worker() {
while (queue.length > 0) {
const code = queue.shift()!
try {
// Полный URL передаём как input (без prefixUrl) + searchParams,
// иначе got добавляет лишний слэш (.../check/?code=) → 404.
const { body: response } = await loggedClient().get<PublicCheckResponse>(
envUrls[env].publicCheckUrl,
{ searchParams: { code }, timeout: { request: 10000 } }
)
const status = response.outerStatus || response.status || 'UNKNOWN'
results.push({
code: response.code || code,
found: response.codeFounded ?? response.codeResolveData?.found ?? false,
valid: VALID_STATUSES.includes(status),
status,
productName: response.productName,
})
} catch {
results.push({
code,
found: false,
valid: false,
status: 'ERROR',
error: 'Public check API error',
})
}
}
}
const workers = Array.from({ length: config.trueApi.publicCheckConcurrency }, () => worker())
await Promise.all(workers)
return buildResponse(results)
}
function getToken(env: TrueApiEnv): string {
const token = getCachedToken(env)
if (!token) throw new AppError('Not authenticated', 401)
return token
}
export async function checkCodesAuth(
env: TrueApiEnv,
body: CheckCodesRequest,
log?: (msg: string, data?: Record<string, unknown>) => void,
): Promise<CheckCodesResponse> {
const token = getToken(env)
const results: SingleCodeResult[] = []
let debugInfo: ApiDebugInfo | undefined
try {
await processBatch(env, body.codes, token, results, 0)
} catch (err) {
if (err instanceof AppError && err.statusCode === 401) {
clearToken(env)
throw err
}
if (err instanceof AppError) {
debugInfo = err.debugInfo
if (log) log('Auth API error (non-401)', { error: err.message, debugInfo })
} else {
debugInfo = extractDebugInfo(err)
if (log) log('Auth API error (non-401)', { error: String(err), debugInfo })
}
for (const code of body.codes) {
if (!results.find(r => r.code === code)) {
results.push({ code, found: false, valid: false, status: 'ERROR', error: 'Auth API error' })
}
}
}
return buildResponse(results, debugInfo)
}
function buildResponse(results: SingleCodeResult[], debugInfo?: ApiDebugInfo): CheckCodesResponse {
return {
results,
total: results.length,
validCount: results.filter(r => r.valid).length,
invalidCount: results.filter(r => !r.valid && !r.error).length,
errorCount: results.filter(r => !!r.error).length,
debugInfo,
}
}
function extractRequestHeaders(options: Record<string, unknown> | undefined): Record<string, string> {
const reqHeaders = options?.headers as Record<string, string> | undefined
const clean: Record<string, string> = {}
if (reqHeaders) {
for (const [k, v] of Object.entries(reqHeaders)) {
clean[k] = k.toLowerCase() === 'authorization' ? 'Bearer ***' : String(v)
}
}
return clean
}
export function extractDebugInfoFromResponse<T>(response: Response<T>): ApiDebugInfo {
const req = response.request as unknown as Record<string, unknown> | undefined
const opts = req?.options as Record<string, unknown> | undefined
return {
request: {
method: String(opts?.method || 'POST'),
url: String(response.requestUrl || response.url || 'unknown'),
headers: extractRequestHeaders(opts),
body: (opts?.json || opts?.body) ?? null,
},
response: {
statusCode: response.statusCode,
headers: (response.headers || {}) as Record<string, string>,
body: response.body ?? null,
},
}
}
export function extractDebugInfo(err: unknown): ApiDebugInfo | undefined {
if (!err || typeof err !== 'object') return undefined
const gotErr = err as Record<string, unknown>
const response = gotErr.response as Record<string, unknown> | undefined
const options = gotErr.options as Record<string, unknown> | undefined
const code = gotErr.code ? String(gotErr.code) : undefined
const message = gotErr.message ? String(gotErr.message) : undefined
const cleanHeaders = extractRequestHeaders(options)
let reqUrl = 'unknown'
if (response?.requestUrl) {
reqUrl = String(response.requestUrl)
} else if (options) {
const u = (options as Record<string, unknown>).url
if (u) reqUrl = String(u)
else if ((options as Record<string, unknown>).href) reqUrl = String((options as Record<string, unknown>).href)
else if ((options as Record<string, unknown>).pathname) reqUrl = String((options as Record<string, unknown>).pathname)
}
const resHeaders = response?.headers as Record<string, string> | undefined
return {
request: {
method: String(options?.method || 'POST'),
url: reqUrl,
headers: cleanHeaders,
body: options?.json || options?.body || null,
},
response: {
statusCode: Number(response?.statusCode || 0),
headers: resHeaders || {},
body: response?.body || null,
errorCode: code,
errorMessage: message,
},
}
}
async function processBatch(
env: TrueApiEnv, codes: string[], token: string, results: SingleCodeResult[], batchIndex: number
): Promise<void> {
if (batchIndex >= codes.length) return
const start = batchIndex
const end = Math.min(start + config.trueApi.batchSize, codes.length)
const batch = codes.slice(start, end)
await retryOnError(env, batch, token, results)
await processBatch(env, codes, token, results, end)
}
async function retryOnError(
env: TrueApiEnv, batch: string[], token: string, results: SingleCodeResult[], attempt = 0
): Promise<void> {
const response = await apiClient(env).post<CisInfoResponseItem[]>(
config.trueApi.cisesInfoPath,
{
json: batch,
headers: { Authorization: `Bearer ${token}` },
timeout: { request: 30000 },
throwHttpErrors: false,
}
)
if (response.statusCode === 401) {
const debugInfo = extractDebugInfoFromResponse(response)
throw new AppError('Token expired', 401, debugInfo)
}
if (response.statusCode === 429 || (response.statusCode >= 500 && attempt < config.trueApi.maxRetries)) {
const delay = Math.pow(2, attempt) * 500
await sleep(delay)
return retryOnError(env, batch, token, results, attempt + 1)
}
if (!response.body || !Array.isArray(response.body)) {
const debugInfo = extractDebugInfoFromResponse(response)
const apiErr = response.body && typeof response.body === 'object'
? (response.body as Record<string, unknown>).error_message || ''
: ''
throw new AppError(`API error: ${apiErr || response.statusCode}`, response.statusCode, debugInfo)
}
const items = response.body as CisInfoResponseItem[]
for (const item of items) {
const ci = item.cisInfo
results.push({
code: ci.requestedCis,
found: !!ci.cis && !item.errorCode,
valid: VALID_STATUSES.includes(ci.status || ''),
status: item.errorCode || ci.status || 'UNKNOWN',
gtin: ci.gtin,
productName: ci.productName,
producerName: ci.producerName,
ownerName: ci.ownerName,
})
}
}
function sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms))
}
export class AppError extends Error {
statusCode: number
debugInfo?: ApiDebugInfo
constructor(message: string, statusCode: number, debugInfo?: ApiDebugInfo) {
super(message)
this.statusCode = statusCode
this.debugInfo = debugInfo
}
}
-32
View File
@@ -1,32 +0,0 @@
import NodeCache from 'node-cache'
import { config } from '../config.js'
import type { TrueApiEnv } from '../config.js'
const cache = new NodeCache({
stdTTL: config.trueApi.tokenTTL / 1000,
checkperiod: 600,
})
function tokenKey(env: TrueApiEnv): string {
return `true_api_token_${env}`
}
export function getToken(env: TrueApiEnv): string | undefined {
return cache.get<string>(tokenKey(env))
}
export function setToken(env: TrueApiEnv, token: string): void {
cache.set(tokenKey(env), token)
}
export function hasToken(env: TrueApiEnv): boolean {
return cache.has(tokenKey(env))
}
export function getTokenTTL(env: TrueApiEnv): number | undefined {
return cache.getTtl(tokenKey(env))
}
export function invalidateToken(env: TrueApiEnv): void {
cache.del(tokenKey(env))
}
-70
View File
@@ -1,70 +0,0 @@
import { parseStringPromise } from 'xml2js'
export interface ParsedUpd {
codes: string[]
fileName: string
documentNumber?: string
documentDate?: string
sellerName?: string
buyerName?: string
}
export async function parseUpdXml(xmlContent: string, fileName: string): Promise<ParsedUpd> {
const parsed = await parseStringPromise(xmlContent, {
ignoreAttrs: false,
mergeAttrs: true,
})
const codes = extractCodes(parsed)
return {
codes,
fileName,
documentNumber: extractField(parsed, ['Документ', 'НомерДок']),
documentDate: extractField(parsed, ['Документ', 'ДатаДок']),
sellerName: extractField(parsed, ['Документ', 'ТаблСчФакт', 'СведПрод', 'НаимОрг']),
buyerName: extractField(parsed, ['Документ', 'ТаблСчФакт', 'СведПокуп', 'НаимОрг']),
}
}
function extractCodes(obj: Record<string, unknown>): string[] {
const codes: string[] = []
const documents = asArray<Record<string, unknown>>(obj['Документ'])
for (const document of documents) {
try {
const table = document['ТаблСчФакт'] as Record<string, unknown> | undefined
if (!table) continue
const items = asArray<Record<string, unknown>>(table['СведТов'])
for (const item of items) {
const extInfo = asArray<Record<string, unknown>>(item['ДопСведТов'])
for (const info of extInfo) {
const kmCodes = asArray<string>(info['НомСредИдентТов'])
codes.push(...kmCodes.filter(Boolean))
}
}
} catch {
console.warn('upd-parser: failed to extract codes from a document')
}
}
return codes
}
function asArray<T>(value: unknown): T[] {
if (value == null) return []
return Array.isArray(value) ? value as T[] : [value as T]
}
function extractField(obj: Record<string, unknown>, keys: string[]): string | undefined {
let current: unknown = obj
for (const key of keys) {
if (current && typeof current === 'object') {
current = (current as Record<string, unknown>)[key]
} else {
return undefined
}
}
return typeof current === 'string' ? current : undefined
}
-93
View File
@@ -1,93 +0,0 @@
export interface AuthKeyResponse {
uuid: string
data: string
}
export interface AuthSignInRequest {
uuid: string
data: string
inn?: string
}
export interface AuthSignInResponse {
token: string
uuidToken?: string
expireDate?: string
}
export interface CisInfoItem {
requestedCis: string
cis?: string
gtin?: string
status?: string
productName?: string
productGroup?: string
producerName?: string
ownerName?: string
ownerBin?: string
producedDate?: string
packageType?: string
error?: string
}
export interface CisInfoResponseItem {
cisInfo: CisInfoItem
errorMessage?: string | null
errorCode?: string | null
}
export interface PublicCheckResponse {
code: string
// Схема ответа mobile.api.crpt.ru/mobile/check (эндпоинт мобильного приложения)
codeFounded?: boolean
checkResult?: boolean
status?: string
outerStatus?: string
productName?: string
codeResolveData?: {
valid?: boolean
found?: boolean
verified?: boolean
}
}
export interface CheckCodesRequest {
codes: string[]
}
export interface ApiDebugInfo {
request: {
method: string
url: string
headers: Record<string, string>
body: unknown
}
response: {
statusCode: number
headers: Record<string, string>
body: unknown
errorCode?: string
errorMessage?: string
}
}
export interface SingleCodeResult {
code: string
found: boolean
valid: boolean
status: string
gtin?: string
productName?: string
producerName?: string
ownerName?: string
error?: string
}
export interface CheckCodesResponse {
results: SingleCodeResult[]
total: number
validCount: number
invalidCount: number
errorCount: number
debugInfo?: ApiDebugInfo
}
-19
View File
@@ -1,19 +0,0 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"outDir": "dist",
"rootDir": "src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}