Passed
Push — main ( 7d04df...f2cd2d )
by Yuri
01:12 queued 13s
created

parser.ts ➔ parseReceipt   A

Complexity

Conditions 4

Size

Total Lines 27
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 4.9102

Importance

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