Passed
Push — main ( 728453...bc4917 )
by Yuri
01:21
created

src/parser.ts   A

Complexity

Total Complexity 22
Complexity/F 1.47

Size

Lines of Code 137
Function Count 15

Duplication

Duplicated Lines 0
Ratio 0 %

Test Coverage

Coverage 98.11%

Importance

Changes 0
Metric Value
wmc 22
eloc 107
mnd 7
bc 7
fnc 15
dl 0
loc 137
bpm 0.4666
cpm 1.4665
noi 0
c 0
b 0
f 0
ccs 52
cts 53
cp 0.9811
rs 10

15 Functions

Rating   Name   Duplication   Size   Complexity  
A ReceiptParser.extractSequencesFromContent 0 5 1
A ReceiptParser.removeDuplicates 0 3 1
A ReceiptParser.validateParsedFields 0 7 2
A parser.ts ➔ parseReceipt 0 3 1
A ReceiptParser.addToArrayFieldIfApplicable 0 10 2
A ReceiptParser.addFieldToReceipt 0 4 1
A ReceiptParser.parseReceipt 0 14 2
A ReceiptParser.createInitialParsedReceipt 0 6 1
A ReceiptParser.parseReceiptContent 0 4 1
A ReceiptParser.processSequence 0 5 2
A ReceiptParser.deduplicateArrayFields 0 4 1
A ReceiptParser.extractStringValue 0 9 2
A ReceiptParser.handleVerifiedSequence 0 7 1
A ReceiptParser.getFieldHandler 0 12 3
A ReceiptParser.isValidReceiptFieldKey 0 3 1
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
export type ParsedReceipt = Partial<Record<ReceiptFieldsKeyNames, string>> & {
10
  ENVIRONMENT: Environment
11
  IN_APP_ORIGINAL_TRANSACTION_IDS: string[]
12
  IN_APP_TRANSACTION_IDS: string[]
13
}
14
15
class ReceiptParser {
16
  private readonly parsed: ParsedReceipt
17
18
  constructor() {
19 6
    this.parsed = this.createInitialParsedReceipt()
20
  }
21
22
  public parseReceipt(receipt: string): ParsedReceipt {
23 6
    if (typeof receipt !== 'string' || receipt.trim() === '') {
24 2
      throw new Error('Receipt must be a non-empty string.')
25
    }
26
27 4
    const rootSchemaVerification = verifyReceiptSchema(receipt)
28 3
    const content = rootSchemaVerification.result[CONTENT_ID] as ASN1.OctetString
29
30 3
    this.parseReceiptContent(content)
31 3
    this.validateParsedFields()
32 3
    this.deduplicateArrayFields()
33
34 3
    return this.parsed
35
  }
36
37
  private createInitialParsedReceipt(): ParsedReceipt {
38 6
    return {
39
      ENVIRONMENT: 'Production',
40
      IN_APP_ORIGINAL_TRANSACTION_IDS: [],
41
      IN_APP_TRANSACTION_IDS: [],
42
    }
43
  }
44
45
  private parseReceiptContent(content: ASN1.OctetString): void {
46 18
    const sequences = this.extractSequencesFromContent(content)
47 18
    sequences.forEach(this.processSequence.bind(this))
48
  }
49
50
  private extractSequencesFromContent(content: ASN1.OctetString): ASN1.Sequence[] {
51 18
    const [contentSet] = content.valueBlock.value as ASN1.Set[]
52 18
    return contentSet.valueBlock.value
53 348
      .filter(v => v instanceof ASN1.Sequence) as ASN1.Sequence[]
54
  }
55
56
  private processSequence(sequence: ASN1.Sequence): void {
57 348
    const verifiedSequence = verifyFieldSchema(sequence)
58 348
    if (verifiedSequence) {
59 348
      this.handleVerifiedSequence(verifiedSequence)
60
    }
61
  }
62
63
  private handleVerifiedSequence(verifiedSequence: ASN1.CompareSchemaSuccess): void {
64 348
    const fieldKey = (verifiedSequence.result[FIELD_TYPE_ID] as ASN1.Integer).valueBlock.valueDec
65 348
    const fieldValue = verifiedSequence.result[FIELD_VALUE_ID] as ASN1.OctetString
66
67 348
    const handler = this.getFieldHandler(fieldKey)
68 348
    handler(fieldValue)
69
  }
70
71
  private getFieldHandler(fieldKey: number): (fieldValue: ASN1.OctetString) => void {
72 348
    if (fieldKey === IN_APP) {
73 15
      return this.parseReceiptContent.bind(this)
74
    }
75 333
    if (this.isValidReceiptFieldKey(fieldKey)) {
76 159
      const name = RECEIPT_FIELDS_MAP.get(fieldKey)!
77 159
      return (fieldValue: ASN1.OctetString) => {
78 159
        this.addFieldToReceipt(name, this.extractStringValue(fieldValue))
79
      }
80
    }
81 174
    return () => {}
82
  }
83
84
  private isValidReceiptFieldKey(value: unknown): value is ReceiptFieldsKeyValues {
85 333
    return typeof value === 'number' && RECEIPT_FIELDS_MAP.has(value as ReceiptFieldsKeyValues)
86
  }
87
88
  private extractStringValue(field: ASN1.OctetString): string {
89 159
    const [fieldValue] = field.valueBlock.value
90
91 159
    if (fieldValue instanceof ASN1.IA5String || fieldValue instanceof ASN1.Utf8String) {
92 123
      return fieldValue.valueBlock.value
93
    }
94
95 36
    return field.toJSON().valueBlock.valueHex
96
  }
97
98
  private addFieldToReceipt(name: ReceiptFieldsKeyNames, value: string): void {
99 159
    this.addToArrayFieldIfApplicable(name, value)
100 159
    this.parsed[name] = value
101
  }
102
103
  private addToArrayFieldIfApplicable(name: ReceiptFieldsKeyNames, value: string): void {
104 159
    const arrayFields: Record<string, keyof ParsedReceipt> = {
105
      'IN_APP_ORIGINAL_TRANSACTION_ID': 'IN_APP_ORIGINAL_TRANSACTION_IDS',
106
      'IN_APP_TRANSACTION_ID': 'IN_APP_TRANSACTION_IDS',
107
    }
108
109 159
    const arrayFieldName = arrayFields[name]
110 159
    if (arrayFieldName) {
111 30
      (this.parsed[arrayFieldName] as string[]).push(value)
112
    }
113
  }
114
115
  private validateParsedFields(): void {
116 3
    const missingFields = Array.from(RECEIPT_FIELDS_MAP.values())
117 51
      .filter(fieldKey => !(fieldKey in this.parsed))
118
119 3
    if (missingFields.length > 0) {
120
      throw new Error(`Missing required fields: ${missingFields.join(', ')}`)
121
    }
122
  }
123
124
  private deduplicateArrayFields(): void {
125 3
    this.parsed.IN_APP_ORIGINAL_TRANSACTION_IDS = this.removeDuplicates(this.parsed.IN_APP_ORIGINAL_TRANSACTION_IDS)
126 3
    this.parsed.IN_APP_TRANSACTION_IDS = this.removeDuplicates(this.parsed.IN_APP_TRANSACTION_IDS)
127
  }
128
129
  private removeDuplicates(array: string[]): string[] {
130 6
    return [...new Set(array)]
131
  }
132
}
133
134 1
export function parseReceipt(receipt: string): ParsedReceipt {
135 6
  return new ReceiptParser().parseReceipt(receipt)
136
}
137