Passed
Push — main ( c5ebc8...59bcdb )
by Yuri
02:02
created

src/parser.ts   A

Complexity

Total Complexity 20
Complexity/F 2.5

Size

Lines of Code 117
Function Count 8

Duplication

Duplicated Lines 0
Ratio 0 %

Test Coverage

Coverage 88%

Importance

Changes 0
Metric Value
wmc 20
eloc 93
mnd 12
bc 12
fnc 8
dl 0
loc 117
ccs 44
cts 50
cp 0.88
bpm 1.5
cpm 2.5
noi 0
c 0
b 0
f 0
rs 10

8 Functions

Rating   Name   Duplication   Size   Complexity  
A parser.ts ➔ postprocessParsedReceipt 0 4 1
A parser.ts ➔ parseOctetStringContent 0 14 3
A parser.ts ➔ isParsedReceiptContentComplete 0 9 3
A parser.ts ➔ extractFieldValue 0 9 2
A parser.ts ➔ parseReceipt 0 28 4
A parser.ts ➔ appendField 0 11 3
A parser.ts ➔ isReceiptFieldKey 0 3 1
A parser.ts ➔ processField 0 13 3
1 1
import * as ASN1 from 'asn1js'
2
3 1
import { RECEIPT_FIELDS_MAP, ReceiptFieldsKeyNames, ReceiptFieldsKeyValues } from './mappings'
4 1
import { CONTENT_ID, FIELD_TYPE_ID, FIELD_VALUE_ID, IN_APP } from './constants'
5 1
import { verifyFieldSchema, verifyReceiptSchema } from './verifications'
6
7
export type Environment = 'Production' | 'ProductionSandbox' | string
8
9 4
const uniqueArrayValues = (array: string[]) => Array.from(new Set(array))
10
11
export type ParsedReceipt = Partial<Record<ReceiptFieldsKeyNames, string>> & {
12
  ENVIRONMENT: Environment
13
  IN_APP_ORIGINAL_TRANSACTION_IDS: string[]
14
  IN_APP_TRANSACTION_IDS: string[]
15
}
16
17
function isReceiptFieldKey (value: unknown): value is ReceiptFieldsKeyValues {
18 186
  return Boolean(typeof value === 'number' && RECEIPT_FIELDS_MAP.has(value as ReceiptFieldsKeyValues))
19
}
20
21
function isParsedReceiptContentComplete (data: ParsedReceipt): data is ParsedReceipt {
22 2
  for (const fieldKey of RECEIPT_FIELDS_MAP.values()) {
23 34
    if (!(fieldKey in data)) {
24
      return false
25
    }
26
  }
27
28 2
  return true
29
}
30
31
function extractFieldValue (field: ASN1.OctetString): string {
32 88
  const [fieldValue] = field.valueBlock.value
33
34 88
  if (fieldValue instanceof ASN1.IA5String || fieldValue instanceof ASN1.Utf8String) {
35 68
    return fieldValue.valueBlock.value
36
  }
37
38 20
  return field.toJSON().valueBlock.valueHex
39
}
40
41
function appendField (parsed: ParsedReceipt, name: ReceiptFieldsKeyNames, value: string) {
42 88
  if (name === 'IN_APP_ORIGINAL_TRANSACTION_ID') {
43 8
    parsed.IN_APP_ORIGINAL_TRANSACTION_IDS.push(value)
44
  }
45
46 88
  if (name === 'IN_APP_TRANSACTION_ID') {
47 8
    parsed.IN_APP_TRANSACTION_IDS.push(value)
48
  }
49
50 88
  parsed[name] = value
51
}
52
53
function processField (parsed: ParsedReceipt, fieldKey: number, fieldValue: ASN1.OctetString) {
54 194
  if (fieldKey === IN_APP) {
55 8
    parseOctetStringContent(parsed, fieldValue)
56 8
    return
57
  }
58
59 186
  if (!isReceiptFieldKey(fieldKey)) {
60 98
    return
61
  }
62
63 88
  const name = RECEIPT_FIELDS_MAP.get(fieldKey)!
64 88
  appendField(parsed, name, extractFieldValue(fieldValue))
65
}
66
67
function parseOctetStringContent (parsed: ParsedReceipt, content: ASN1.OctetString) {
68 10
  const [contentSet] = content.valueBlock.value as ASN1.Set[]
69 10
  const contentSetSequences = contentSet.valueBlock.value
70 194
    .filter(v => v instanceof ASN1.Sequence) as ASN1.Sequence[]
71
72 10
  for (const sequence of contentSetSequences) {
73 194
    const verifiedSequence = verifyFieldSchema(sequence)
74 194
    if (verifiedSequence) {
75
      // We are confident to use "as" assertion because Integer type is guaranteed by positive verification above
76 194
      const fieldKey = (verifiedSequence.result[FIELD_TYPE_ID] as ASN1.Integer).valueBlock.valueDec
77 194
      const fieldValueOctetString = verifiedSequence.result[FIELD_VALUE_ID] as ASN1.OctetString
78
79 194
      processField(parsed, fieldKey, fieldValueOctetString)
80
    }
81
  }
82
}
83
84
function postprocessParsedReceipt (parsed: ParsedReceipt) {
85 2
  parsed.IN_APP_ORIGINAL_TRANSACTION_IDS = uniqueArrayValues(parsed.IN_APP_ORIGINAL_TRANSACTION_IDS)
86 2
  parsed.IN_APP_TRANSACTION_IDS = uniqueArrayValues(parsed.IN_APP_TRANSACTION_IDS)
87
}
88
89 1
export function parseReceipt (receipt: string): ParsedReceipt {
90 3
  const rootSchemaVerification = verifyReceiptSchema(receipt)
91
92 2
  const content = rootSchemaVerification.result[CONTENT_ID] as ASN1.OctetString
93 2
  const parsed: ParsedReceipt = {
94
    ENVIRONMENT: 'Production',
95
    IN_APP_ORIGINAL_TRANSACTION_IDS: [],
96
    IN_APP_TRANSACTION_IDS: [],
97
  }
98
99 2
  parseOctetStringContent(parsed, content)
100
101
  // Verify if the parsed content contains all the required fields
102 2
  if (!isParsedReceiptContentComplete(parsed)) {
103
    const missingProps = []
104
    for (const fieldKey of RECEIPT_FIELDS_MAP.values()) {
105
      if (!(fieldKey in parsed)) {
106
        missingProps.push(fieldKey)
107
      }
108
    }
109
110
    throw new Error(`Missing required fields: ${missingProps.join(', ')}`)
111
  }
112
113 2
  postprocessParsedReceipt(parsed)
114
115 2
  return parsed as ParsedReceipt
116
}
117