mirror of
https://github.com/valitydev/control-center.git
synced 2024-11-06 02:25:17 +00:00
IMP-68: Bulk create chargebacks (#236)
This commit is contained in:
parent
2672d9c670
commit
ecdd62b761
@ -52,7 +52,8 @@
|
||||
"@vality/repairer-proto",
|
||||
"@vality/fistful-proto",
|
||||
"@vality/file-storage-proto",
|
||||
"@vality/thrift-ts"
|
||||
"@vality/thrift-ts",
|
||||
"papaparse"
|
||||
]
|
||||
},
|
||||
"configurations": {
|
||||
|
@ -2,5 +2,5 @@
|
||||
"$schema": "https://raw.githubusercontent.com/streetsidesoftware/cspell/main/cspell.schema.json",
|
||||
"version": "0.2",
|
||||
"import": "node_modules/@vality/cspell-config/cspell.config.js",
|
||||
"words": ["nspkmir", "applepay", "samsungpay", "googlepay", "submain"]
|
||||
"words": ["nspkmir", "applepay", "samsungpay", "googlepay", "submain", "papaparse"]
|
||||
}
|
||||
|
24
package-lock.json
generated
24
package-lock.json
generated
@ -29,7 +29,7 @@
|
||||
"@vality/dominant-cache-proto": "2.0.1-99f38c9.0",
|
||||
"@vality/fistful-proto": "2.0.1-4ff4ea3.0",
|
||||
"@vality/magista-proto": "2.0.1-cf0eff8.0",
|
||||
"@vality/ng-core": "16.1.1-pr-28-a47f5a4.0",
|
||||
"@vality/ng-core": "16.1.1-pr-28-042958a.0",
|
||||
"@vality/payout-manager-proto": "2.0.1-b079679.0",
|
||||
"@vality/repairer-proto": "2.0.1-8f7973d.0",
|
||||
"@vality/thrift-ts": "2.4.1-8ad5123.0",
|
||||
@ -46,6 +46,7 @@
|
||||
"moment": "2.29.4",
|
||||
"monaco-editor": "0.21.2",
|
||||
"ngx-mat-select-search": "7.0.2",
|
||||
"papaparse": "5.4.1",
|
||||
"rxjs": "7.8.1",
|
||||
"short-uuid": "4.2.2",
|
||||
"tslib": "2.3.1",
|
||||
@ -65,6 +66,7 @@
|
||||
"@types/jasmine": "4.0.3",
|
||||
"@types/jwt-decode": "2.2.1",
|
||||
"@types/lodash-es": "4.17.6",
|
||||
"@types/papaparse": "5.3.7",
|
||||
"@vality/cspell-config": "0.1.1-pr-15-020121f.0",
|
||||
"@vality/eslint-config": "1.0.1-pr-27-18c018c.0",
|
||||
"@vality/prettier-config": "0.1.1-pr-15-225ffc3.0",
|
||||
@ -5262,6 +5264,15 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.3.1.tgz",
|
||||
"integrity": "sha512-EhcH/wvidPy1WeML3TtYFGR83UzjxeWRen9V402T8aUGYsCHOmfoisV3ZSg03gAFIbLq8TnWOJ0f4cALtnSEUg=="
|
||||
},
|
||||
"node_modules/@types/papaparse": {
|
||||
"version": "5.3.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/papaparse/-/papaparse-5.3.7.tgz",
|
||||
"integrity": "sha512-f2HKmlnPdCvS0WI33WtCs5GD7X1cxzzS/aduaxSu3I7TbhWlENjSPs6z5TaB9K0J+BH1jbmqTaM+ja5puis4wg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/qs": {
|
||||
"version": "6.9.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz",
|
||||
@ -5776,9 +5787,9 @@
|
||||
"integrity": "sha512-59ncaJpt7tXFLOq9KrDu4OgrDQr9vTQ3j30T0hjN+ZIsPBsE+lld/pGKASWLLQfwvTtvp9laAuKgQGX9GuvIiQ=="
|
||||
},
|
||||
"node_modules/@vality/ng-core": {
|
||||
"version": "16.1.1-pr-28-a47f5a4.0",
|
||||
"resolved": "https://registry.npmjs.org/@vality/ng-core/-/ng-core-16.1.1-pr-28-a47f5a4.0.tgz",
|
||||
"integrity": "sha512-VVx7UdNT/8h0E0q+swLHD7rarm8wMXwBjINFYz47KEkQrWGVw7NFP38r6thFAQMWB9Knf5mR4RykdJ+LdDFduQ==",
|
||||
"version": "16.1.1-pr-28-042958a.0",
|
||||
"resolved": "https://registry.npmjs.org/@vality/ng-core/-/ng-core-16.1.1-pr-28-042958a.0.tgz",
|
||||
"integrity": "sha512-gIvMwBCj2TRTWuAj2MevzfjZijC+X8qoh5ByBDNtqPhBOPBa/AxARtT7CV8Si92ww4NTlyxfZ0QhSStQaRJSmQ==",
|
||||
"dependencies": {
|
||||
"@ng-matero/extensions": "^16.0.0",
|
||||
"@s-libs/js-core": "^16.0.0",
|
||||
@ -17655,6 +17666,11 @@
|
||||
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
|
||||
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="
|
||||
},
|
||||
"node_modules/papaparse": {
|
||||
"version": "5.4.1",
|
||||
"resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.4.1.tgz",
|
||||
"integrity": "sha512-HipMsgJkZu8br23pW15uvo6sib6wne/4woLZPlFf3rpDyMe9ywEXUsuD7+6K9PRkJlVT51j/sCOYDKGGS3ZJrw=="
|
||||
},
|
||||
"node_modules/parallel-transform": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/parallel-transform/-/parallel-transform-1.2.0.tgz",
|
||||
|
@ -37,7 +37,7 @@
|
||||
"@vality/dominant-cache-proto": "2.0.1-99f38c9.0",
|
||||
"@vality/fistful-proto": "2.0.1-4ff4ea3.0",
|
||||
"@vality/magista-proto": "2.0.1-cf0eff8.0",
|
||||
"@vality/ng-core": "16.1.1-pr-28-a47f5a4.0",
|
||||
"@vality/ng-core": "16.1.1-pr-28-042958a.0",
|
||||
"@vality/payout-manager-proto": "2.0.1-b079679.0",
|
||||
"@vality/repairer-proto": "2.0.1-8f7973d.0",
|
||||
"@vality/thrift-ts": "2.4.1-8ad5123.0",
|
||||
@ -54,6 +54,7 @@
|
||||
"moment": "2.29.4",
|
||||
"monaco-editor": "0.21.2",
|
||||
"ngx-mat-select-search": "7.0.2",
|
||||
"papaparse": "5.4.1",
|
||||
"rxjs": "7.8.1",
|
||||
"short-uuid": "4.2.2",
|
||||
"tslib": "2.3.1",
|
||||
@ -73,6 +74,7 @@
|
||||
"@types/jasmine": "4.0.3",
|
||||
"@types/jwt-decode": "2.2.1",
|
||||
"@types/lodash-es": "4.17.6",
|
||||
"@types/papaparse": "5.3.7",
|
||||
"@vality/cspell-config": "0.1.1-pr-15-020121f.0",
|
||||
"@vality/eslint-config": "1.0.1-pr-27-18c018c.0",
|
||||
"@vality/prettier-config": "0.1.1-pr-15-225ffc3.0",
|
||||
|
@ -47,6 +47,6 @@
|
||||
(update)="load($event)"
|
||||
>
|
||||
<!-- <button color="primary" mat-button>Change statuses</button>-->
|
||||
<!-- <button color="primary" mat-button (click)="create()">Create</button>-->
|
||||
<button color="primary" mat-button (click)="create()">Create by file</button>
|
||||
</cc-chargebacks-table>
|
||||
</cc-page-layout>
|
||||
|
@ -12,10 +12,11 @@ import {
|
||||
isEqualDateRange,
|
||||
countProps,
|
||||
DialogService,
|
||||
DialogResponseStatus,
|
||||
} from '@vality/ng-core';
|
||||
import { endOfDay } from 'date-fns';
|
||||
import merge from 'lodash-es/merge';
|
||||
import { debounceTime } from 'rxjs';
|
||||
import { debounceTime, filter } from 'rxjs';
|
||||
import { startWith } from 'rxjs/operators';
|
||||
|
||||
import {
|
||||
@ -24,7 +25,7 @@ import {
|
||||
CHARGEBACK_CATEGORIES,
|
||||
} from '@cc/app/api/fistful-stat';
|
||||
|
||||
import { CreateChargebackDialogComponent } from './components/create-chargeback-dialog/create-chargeback-dialog.component';
|
||||
import { CreateChargebacksByFileDialogComponent } from './components/create-chargebacks-by-file-dialog/create-chargebacks-by-file-dialog.component';
|
||||
import { FetchChargebacksService } from './fetch-chargebacks.service';
|
||||
|
||||
@UntilDestroy()
|
||||
@ -112,6 +113,15 @@ export class ChargebacksComponent implements OnInit {
|
||||
}
|
||||
|
||||
create() {
|
||||
this.dialog.open(CreateChargebackDialogComponent, {});
|
||||
this.dialog
|
||||
.open(CreateChargebacksByFileDialogComponent)
|
||||
.afterClosed()
|
||||
.pipe(
|
||||
filter((res) => res.status === DialogResponseStatus.Success),
|
||||
untilDestroyed(this)
|
||||
)
|
||||
.subscribe(() => {
|
||||
this.load();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -3,8 +3,11 @@ import { NgModule } from '@angular/core';
|
||||
import { FlexModule } from '@angular/flex-layout';
|
||||
import { ReactiveFormsModule } from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
import { MatOptionModule } from '@angular/material/core';
|
||||
import { MatExpansionModule } from '@angular/material/expansion';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import {
|
||||
TableModule,
|
||||
@ -14,6 +17,7 @@ import {
|
||||
InputFieldModule,
|
||||
SelectFieldModule,
|
||||
DialogModule,
|
||||
FileUploadModule,
|
||||
} from '@vality/ng-core';
|
||||
|
||||
import { PageLayoutModule, ShopFieldModule, ThriftPipesModule } from '../../shared';
|
||||
@ -26,12 +30,14 @@ import { ChargebacksRoutingModule } from './chargebacks-routing.module';
|
||||
import { ChargebacksComponent } from './chargebacks.component';
|
||||
import { ChargebacksTableComponent } from './components/chargebacks-table/chargebacks-table.component';
|
||||
import { CreateChargebackDialogComponent } from './components/create-chargeback-dialog/create-chargeback-dialog.component';
|
||||
import { CreateChargebacksByFileDialogComponent } from './components/create-chargebacks-by-file-dialog/create-chargebacks-by-file-dialog.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
ChargebacksComponent,
|
||||
ChargebacksTableComponent,
|
||||
CreateChargebackDialogComponent,
|
||||
CreateChargebacksByFileDialogComponent,
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
@ -56,6 +62,10 @@ import { CreateChargebackDialogComponent } from './components/create-chargeback-
|
||||
DialogModule,
|
||||
DomainThriftFormComponent,
|
||||
FlexModule,
|
||||
FileUploadModule,
|
||||
MatExpansionModule,
|
||||
MatInputModule,
|
||||
MatCheckboxModule,
|
||||
],
|
||||
})
|
||||
export class ChargebacksModule {}
|
||||
|
@ -1,4 +1,4 @@
|
||||
<v-dialog title="Create chargebacks">
|
||||
<v-dialog title="Create chargeback">
|
||||
<div fxLayout="column" fxLayoutGap="16px">
|
||||
<v-input-field [formControl]="invoiceControl" label="Invoice Id" required></v-input-field>
|
||||
<v-input-field [formControl]="paymentControl" label="Payment Id" required></v-input-field>
|
||||
|
@ -0,0 +1,43 @@
|
||||
<v-dialog [progress]="progress$ | async" title="Create chargebacks">
|
||||
<div fxLayout="column" fxLayoutGap="16px">
|
||||
<mat-checkbox [formControl]="hasHeaderControl">CSV with header</mat-checkbox>
|
||||
|
||||
<v-file-upload [extensions]="['csv']" (upload)="loadFile($event)"></v-file-upload>
|
||||
|
||||
<mat-accordion>
|
||||
<mat-expansion-panel>
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title>Default format</mat-panel-title>
|
||||
</mat-expansion-panel-header>
|
||||
<div class="mat-body-1">
|
||||
<code style="word-wrap: break-word">{{ defaultFormat }}</code>
|
||||
</div>
|
||||
<div class="mat-body-1">
|
||||
Categories: fraud, dispute, authorisation, processing_error
|
||||
</div>
|
||||
<div class="mat-body-1">Starting with "body" optional</div>
|
||||
<div class="mat-body-1">Separator dot comma (;)</div>
|
||||
</mat-expansion-panel>
|
||||
</mat-accordion>
|
||||
|
||||
<v-table
|
||||
[columns]="columns"
|
||||
[data]="chargebacks$ | async"
|
||||
[rowSelected]="selectedChargebacks"
|
||||
noActions
|
||||
rowSelectable
|
||||
(rowSelectionChange)="selectedChargebacks = $event"
|
||||
></v-table>
|
||||
</div>
|
||||
|
||||
<v-dialog-actions>
|
||||
<button
|
||||
[disabled]="!selectedChargebacks?.length"
|
||||
color="primary"
|
||||
mat-raised-button
|
||||
(click)="create()"
|
||||
>
|
||||
Create
|
||||
</button>
|
||||
</v-dialog-actions>
|
||||
</v-dialog>
|
@ -0,0 +1,114 @@
|
||||
import { Component, Injector, OnInit } from '@angular/core';
|
||||
import { FormControl } from '@angular/forms';
|
||||
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
|
||||
import {
|
||||
DialogSuperclass,
|
||||
NotifyLogService,
|
||||
loadFileContent,
|
||||
DEFAULT_DIALOG_CONFIG,
|
||||
Column,
|
||||
forkJoinToResult,
|
||||
} from '@vality/ng-core';
|
||||
import { BehaviorSubject, combineLatest } from 'rxjs';
|
||||
import { switchMap, map, shareReplay, tap, startWith } from 'rxjs/operators';
|
||||
|
||||
import { InvoicingService } from '@cc/app/api/payment-processing';
|
||||
import { parseCsv, unifyCsvItems } from '@cc/utils';
|
||||
|
||||
import { CsvChargeback } from './types/csv-chargeback';
|
||||
import { CSV_CHARGEBACK_PROPS } from './types/csv-chargeback-props';
|
||||
import { csvChargebacksToInvoicePaymentChargebackParams } from './utils/csv-chargebacks-to-invoice-payment-chargeback-params';
|
||||
|
||||
@UntilDestroy()
|
||||
@Component({
|
||||
selector: 'cc-create-chargebacks-by-file-dialog',
|
||||
templateUrl: './create-chargebacks-by-file-dialog.component.html',
|
||||
})
|
||||
export class CreateChargebacksByFileDialogComponent
|
||||
extends DialogSuperclass<CreateChargebacksByFileDialogComponent>
|
||||
implements OnInit
|
||||
{
|
||||
static defaultDialogConfig = DEFAULT_DIALOG_CONFIG.large;
|
||||
|
||||
hasHeaderControl = new FormControl(true);
|
||||
progress$ = new BehaviorSubject(0);
|
||||
upload$ = new BehaviorSubject<File | null>(null);
|
||||
columns: Column<CsvChargeback>[] = CSV_CHARGEBACK_PROPS.map((c) => ({ field: c, header: c }));
|
||||
defaultFormat = CSV_CHARGEBACK_PROPS.join(';');
|
||||
selectedChargebacks: CsvChargeback[] = [];
|
||||
chargebacks$ = combineLatest([
|
||||
this.upload$,
|
||||
this.hasHeaderControl.valueChanges.pipe(startWith(null)),
|
||||
]).pipe(
|
||||
switchMap(([file]) => loadFileContent(file)),
|
||||
map((content) =>
|
||||
parseCsv(content, { header: this.hasHeaderControl.value || false, delimiter: ';' })
|
||||
),
|
||||
tap((d) => {
|
||||
if (!d.errors.length) return;
|
||||
if (d.errors.length === 1) this.log.error(d.errors[0]);
|
||||
this.log.error(new Error(d.errors.map((e) => e.message).join('. ')));
|
||||
}),
|
||||
map((d) => {
|
||||
const chargebacks = unifyCsvItems(d?.data, CSV_CHARGEBACK_PROPS);
|
||||
if (chargebacks[0].invoice_id) return chargebacks;
|
||||
this.log.error(
|
||||
'Perhaps you incorrectly checked the checkbox to have or not a header (the first element does not have at least an invoice ID)'
|
||||
);
|
||||
return [];
|
||||
}),
|
||||
shareReplay({ refCount: true, bufferSize: 1 })
|
||||
);
|
||||
|
||||
constructor(
|
||||
injector: Injector,
|
||||
private invoicingService: InvoicingService,
|
||||
private log: NotifyLogService
|
||||
) {
|
||||
super(injector);
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.chargebacks$.pipe(untilDestroyed(this)).subscribe((c) => {
|
||||
this.selectedChargebacks = c || [];
|
||||
});
|
||||
}
|
||||
|
||||
create() {
|
||||
const selected = this.selectedChargebacks;
|
||||
forkJoinToResult(
|
||||
selected.map((c) =>
|
||||
this.invoicingService.CreateChargeback(
|
||||
c.invoice_id,
|
||||
c.payment_id,
|
||||
csvChargebacksToInvoicePaymentChargebackParams(c)
|
||||
)
|
||||
),
|
||||
2,
|
||||
this.progress$
|
||||
)
|
||||
.pipe(untilDestroyed(this))
|
||||
.subscribe({
|
||||
next: (res) => {
|
||||
const chargebacksWithError = res.filter((c) => c.hasError);
|
||||
if (chargebacksWithError.length) {
|
||||
this.log.error(
|
||||
chargebacksWithError.map((c) => c.error),
|
||||
`Creating ${chargebacksWithError.length} chargebacks ended in an error. They were re-selected in the table.`
|
||||
);
|
||||
this.selectedChargebacks = chargebacksWithError.map(
|
||||
(c) => selected[c.index]
|
||||
);
|
||||
return;
|
||||
}
|
||||
this.log.successOperation('create', 'chargebacks');
|
||||
this.closeWithSuccess();
|
||||
},
|
||||
error: (err) => this.log.error(err),
|
||||
});
|
||||
}
|
||||
|
||||
async loadFile(file: File) {
|
||||
this.upload$.next(file);
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
export const CSV_CHARGEBACK_PROPS = [
|
||||
'invoice_id',
|
||||
'payment_id',
|
||||
|
||||
'reason.category',
|
||||
'reason.code',
|
||||
|
||||
'levy.amount',
|
||||
'levy.currency.symbolic_code',
|
||||
|
||||
// Optional
|
||||
'body.amount',
|
||||
'body.currency.symbolic_code',
|
||||
|
||||
'external_id',
|
||||
'occurred_at',
|
||||
|
||||
'context.type',
|
||||
'context.data',
|
||||
|
||||
'transaction_info.id',
|
||||
'transaction_info.timestamp',
|
||||
'transaction_info.extra',
|
||||
] as const;
|
@ -0,0 +1,5 @@
|
||||
import { CsvItem } from '@cc/utils';
|
||||
|
||||
import { CSV_CHARGEBACK_PROPS } from './csv-chargeback-props';
|
||||
|
||||
export type CsvChargeback = CsvItem<(typeof CSV_CHARGEBACK_PROPS)[number]>;
|
@ -0,0 +1,56 @@
|
||||
import { InvoicePaymentChargebackParams } from '@vality/domain-proto/payment_processing';
|
||||
import { clean } from '@vality/ng-core';
|
||||
import short from 'short-uuid';
|
||||
|
||||
import { CsvChargeback } from '../types/csv-chargeback';
|
||||
|
||||
export function csvChargebacksToInvoicePaymentChargebackParams(
|
||||
c: CsvChargeback
|
||||
): InvoicePaymentChargebackParams {
|
||||
return clean(
|
||||
{
|
||||
id: short().uuid(),
|
||||
reason: {
|
||||
code: c['reason.code'],
|
||||
category: { [c['reason.category']]: {} },
|
||||
},
|
||||
levy: {
|
||||
amount: c['levy.amount'],
|
||||
currency: {
|
||||
symbolic_code: c['levy.currency.symbolic_code'],
|
||||
},
|
||||
},
|
||||
body: clean(
|
||||
{
|
||||
amount: c['body.amount'],
|
||||
currency: {
|
||||
symbolic_code: c['body.currency.symbolic_code'],
|
||||
},
|
||||
},
|
||||
true
|
||||
),
|
||||
transaction_info: clean(
|
||||
{
|
||||
id: c['transaction_info.id'],
|
||||
timestamp: c['transaction_info.timestamp'],
|
||||
extra: c['transaction_info.extra'],
|
||||
additional_info: c['transaction_info.additional_info']
|
||||
? JSON.parse(c['transaction_info.additional_info'])
|
||||
: undefined,
|
||||
},
|
||||
true
|
||||
),
|
||||
external_id: c.external_id,
|
||||
context: clean(
|
||||
{
|
||||
type: c['context.type'],
|
||||
data: c['context.data'],
|
||||
},
|
||||
true
|
||||
),
|
||||
occurred_at: c['occurred_at'],
|
||||
},
|
||||
false,
|
||||
true
|
||||
);
|
||||
}
|
11
src/app/shared/utils/clean-thrift.ts
Normal file
11
src/app/shared/utils/clean-thrift.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { clean, isEmpty } from '@vality/ng-core';
|
||||
import isObject from 'lodash-es/isObject';
|
||||
|
||||
function isEmptyThrift(value: unknown): boolean {
|
||||
if (isObject(value) && value.constructor === Object) return false;
|
||||
return isEmpty(value);
|
||||
}
|
||||
|
||||
export function cleanThrift<T extends object>(obj: T) {
|
||||
return clean(obj, false, false, (v) => !isEmptyThrift(v));
|
||||
}
|
@ -1,2 +1,3 @@
|
||||
export * from './polling-conditions';
|
||||
export * from './deposit-status';
|
||||
export * from './clean-thrift';
|
||||
|
3
src/utils/csv/index.ts
Normal file
3
src/utils/csv/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from './parse-csv';
|
||||
export * from './types/csv-item';
|
||||
export * from './utils/unify-csv-items';
|
5
src/utils/csv/parse-csv.ts
Normal file
5
src/utils/csv/parse-csv.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import Papa, { ParseConfig } from 'papaparse';
|
||||
|
||||
export function parseCsv(content: string, config?: ParseConfig) {
|
||||
return Papa.parse(content, config);
|
||||
}
|
2
src/utils/csv/types/csv-item.ts
Normal file
2
src/utils/csv/types/csv-item.ts
Normal file
@ -0,0 +1,2 @@
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export type CsvItem<TPropertyKey extends string = string> = Record<TPropertyKey[number], any>;
|
12
src/utils/csv/utils/unify-csv-items.ts
Normal file
12
src/utils/csv/utils/unify-csv-items.ts
Normal file
@ -0,0 +1,12 @@
|
||||
export function unifyCsvItems<T>(
|
||||
csv: T[] | string[][],
|
||||
defaultProps: readonly (keyof T)[] | (keyof T)[]
|
||||
): T[] {
|
||||
if (!Array.isArray(csv)) return [];
|
||||
if (Array.isArray(csv?.[0])) {
|
||||
return csv.map((d) =>
|
||||
Object.fromEntries(d.map((prop, idx) => [defaultProps[idx], prop]))
|
||||
) as T[];
|
||||
}
|
||||
return csv as T[];
|
||||
}
|
@ -14,3 +14,4 @@ export * from './get-enum-keys';
|
||||
export * from './is-nil-or-empty-string';
|
||||
export * from './enumerate';
|
||||
export * from './thrift-instance';
|
||||
export * from './csv';
|
||||
|
Loading…
Reference in New Issue
Block a user