@contrail/flexplm 1.6.1 → 1.7.0-alpha.c295a1e
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -86,8 +86,10 @@ export declare class BaseProcessPublishAssortment {
|
|
|
86
86
|
getResultsCount(events: any): any;
|
|
87
87
|
private sendEvents;
|
|
88
88
|
private sendToFlexPLM;
|
|
89
|
+
private sendAndRecordPublishEvent;
|
|
89
90
|
private saveToLocalFile;
|
|
90
91
|
private handleVibeIQFile;
|
|
92
|
+
private sendPublishPayloadEvent;
|
|
91
93
|
private getCurrentDateString;
|
|
92
94
|
getItemFamilyChanges(pcd: PublishChangeData, changeDetail: any, assortmentItemFullChangeMap: Map<string, any>, assortmentItemDeleteMap: Map<string, any>): Map<string, ItemFamilyChanges>;
|
|
93
95
|
getEventsForPublishChangeData(publishChangeData: PublishChangeData): Promise<SeasonalPayload[]>;
|
|
@@ -595,14 +595,41 @@ class BaseProcessPublishAssortment {
|
|
|
595
595
|
}
|
|
596
596
|
}
|
|
597
597
|
async sendToFlexPLM(events, eventType) {
|
|
598
|
-
const
|
|
598
|
+
const outboundPublishEvent = {
|
|
599
599
|
taskId: this.config.taskId,
|
|
600
600
|
eventType,
|
|
601
601
|
objectClass: 'LCSSeason',
|
|
602
602
|
events
|
|
603
603
|
};
|
|
604
604
|
const flexPLMConnect = new flexplm_connect_1.FlexPLMConnect(this.config);
|
|
605
|
-
|
|
605
|
+
const result = await this.sendAndRecordPublishEvent(flexPLMConnect, outboundPublishEvent);
|
|
606
|
+
const isResultObject = typeof result === 'object' && result !== null;
|
|
607
|
+
return isResultObject
|
|
608
|
+
? { ...result, outboundPublishEvent }
|
|
609
|
+
: { result, outboundPublishEvent };
|
|
610
|
+
}
|
|
611
|
+
async sendAndRecordPublishEvent(flexPLMConnect, outboundPublishEvent) {
|
|
612
|
+
let sendResult;
|
|
613
|
+
try {
|
|
614
|
+
sendResult = await flexPLMConnect.sendToFlexPLM(outboundPublishEvent);
|
|
615
|
+
}
|
|
616
|
+
catch (e) {
|
|
617
|
+
const error = e instanceof Error ? e : new Error(String(e));
|
|
618
|
+
error.outboundPublishEvent = outboundPublishEvent;
|
|
619
|
+
error.sendToFlexPLMResult = { error: e?.message ?? String(e) };
|
|
620
|
+
throw error;
|
|
621
|
+
}
|
|
622
|
+
try {
|
|
623
|
+
await this.sendPublishPayloadEvent(outboundPublishEvent);
|
|
624
|
+
}
|
|
625
|
+
catch (e) {
|
|
626
|
+
const error = e instanceof Error ? e : new Error(String(e));
|
|
627
|
+
error.outboundPublishEvent = outboundPublishEvent;
|
|
628
|
+
error.sendToFlexPLMResult = sendResult;
|
|
629
|
+
error.sendPublishPayloadEventResult = { error: e?.message ?? String(e) };
|
|
630
|
+
throw error;
|
|
631
|
+
}
|
|
632
|
+
return sendResult;
|
|
606
633
|
}
|
|
607
634
|
async saveToLocalFile(events, eventType) {
|
|
608
635
|
let results = {};
|
|
@@ -614,15 +641,16 @@ class BaseProcessPublishAssortment {
|
|
|
614
641
|
const eventDirName = 'events-output';
|
|
615
642
|
const fileName = `${path.sep}${eventDirName}${path.sep}events-${dateString}.json`;
|
|
616
643
|
fileHandle = await fsPromise.open('.' + fileName, 'a');
|
|
617
|
-
const
|
|
644
|
+
const outboundPublishEvent = {
|
|
618
645
|
taskId: this.config.taskId,
|
|
619
646
|
eventType,
|
|
620
647
|
objectClass: 'LCSSeason',
|
|
621
648
|
events
|
|
622
649
|
};
|
|
623
|
-
await fileHandle.writeFile(JSON.stringify(
|
|
650
|
+
await fileHandle.writeFile(JSON.stringify(outboundPublishEvent));
|
|
624
651
|
results = {
|
|
625
652
|
message: 'Successfully Saved File',
|
|
653
|
+
outboundPublishEvent,
|
|
626
654
|
fileLocation: dirName + fileName
|
|
627
655
|
};
|
|
628
656
|
}
|
|
@@ -638,7 +666,7 @@ class BaseProcessPublishAssortment {
|
|
|
638
666
|
const eventBuffer = Buffer.from(JSON.stringify(events), 'utf-8');
|
|
639
667
|
const fileName = 'ASYNC_PUBLISH_SEASON-events.json';
|
|
640
668
|
const uploadFile = await new sdk_1.Files().createAndUploadFileFromBuffer(eventBuffer, 'application/json', fileName, null, this.TTL);
|
|
641
|
-
const
|
|
669
|
+
const outboundPublishEvent = {
|
|
642
670
|
taskId: this.config.taskId,
|
|
643
671
|
eventType,
|
|
644
672
|
objectClass: 'LCSSeason',
|
|
@@ -648,15 +676,39 @@ class BaseProcessPublishAssortment {
|
|
|
648
676
|
};
|
|
649
677
|
if (sendMode === 'vibeiqfile') {
|
|
650
678
|
const flexPLMConnect = new flexplm_connect_1.FlexPLMConnect(this.config);
|
|
651
|
-
|
|
679
|
+
const result = await this.sendAndRecordPublishEvent(flexPLMConnect, outboundPublishEvent);
|
|
680
|
+
const isResultObject = typeof result === 'object' && result !== null;
|
|
681
|
+
return isResultObject
|
|
682
|
+
? { ...result, outboundPublishEvent }
|
|
683
|
+
: { result, outboundPublishEvent };
|
|
652
684
|
}
|
|
653
685
|
else {
|
|
686
|
+
await this.sendPublishPayloadEvent(outboundPublishEvent);
|
|
654
687
|
return {
|
|
655
688
|
message: 'Successfully Uploaded File.',
|
|
656
|
-
|
|
689
|
+
outboundPublishEvent
|
|
657
690
|
};
|
|
658
691
|
}
|
|
659
692
|
}
|
|
693
|
+
async sendPublishPayloadEvent(outboundPublishEvent) {
|
|
694
|
+
console.info('sendPublishPayloadEvent()');
|
|
695
|
+
let initialEvent = {};
|
|
696
|
+
if (this.config?.event) {
|
|
697
|
+
initialEvent = (typeof this.config?.event === 'string')
|
|
698
|
+
? JSON.parse(this.config?.event)
|
|
699
|
+
: this.config?.event;
|
|
700
|
+
}
|
|
701
|
+
const eventBody = {
|
|
702
|
+
originSystemType: 'VibeIQ',
|
|
703
|
+
objectClass: 'AssortmentPublishedToFlexPLM',
|
|
704
|
+
outboundPublishEvent,
|
|
705
|
+
initialEvent
|
|
706
|
+
};
|
|
707
|
+
await new sdk_1.Entities().create({
|
|
708
|
+
entityName: 'external-event',
|
|
709
|
+
object: eventBody
|
|
710
|
+
});
|
|
711
|
+
}
|
|
660
712
|
getCurrentDateString() {
|
|
661
713
|
const d = new Date();
|
|
662
714
|
return '' + d.getUTCFullYear()
|
|
@@ -9,6 +9,7 @@ const sdk_1 = require("@contrail/sdk");
|
|
|
9
9
|
const type_conversion_utils_1 = require("../util/type-conversion-utils");
|
|
10
10
|
const map_utils_1 = require("../util/map-utils");
|
|
11
11
|
const item_family_changes_1 = require("../interfaces/item-family-changes");
|
|
12
|
+
const flexplm_connect_1 = require("../util/flexplm-connect");
|
|
12
13
|
let federatedId = '';
|
|
13
14
|
jest.mock('../util/data-converter', () => {
|
|
14
15
|
return {
|
|
@@ -31,6 +32,13 @@ jest.mock('../util/federation', () => {
|
|
|
31
32
|
});
|
|
32
33
|
let entityObject = {};
|
|
33
34
|
let getOptionsObject = {};
|
|
35
|
+
let createCallArg = undefined;
|
|
36
|
+
let fileUploadCalls = [];
|
|
37
|
+
let fileUploadResult = {
|
|
38
|
+
id: 'file-123',
|
|
39
|
+
downloadUrl: 'https://download.url',
|
|
40
|
+
adminDownloadUrl: 'https://admin.download.url'
|
|
41
|
+
};
|
|
34
42
|
jest.mock('@contrail/sdk', () => {
|
|
35
43
|
return {
|
|
36
44
|
Entities: class {
|
|
@@ -38,6 +46,18 @@ jest.mock('@contrail/sdk', () => {
|
|
|
38
46
|
getOptionsObject = _getOtionsObject;
|
|
39
47
|
return entityObject;
|
|
40
48
|
}
|
|
49
|
+
create(arg) {
|
|
50
|
+
createCallArg = arg;
|
|
51
|
+
return Promise.resolve({});
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
Files: class {
|
|
55
|
+
createAndUploadFileFromBuffer(buffer, mimeType, fileName, _x, ttl) {
|
|
56
|
+
fileUploadCalls.push({ buffer, mimeType, fileName, ttl });
|
|
57
|
+
return Promise.resolve(fileUploadResult);
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
Request: class {
|
|
41
61
|
}
|
|
42
62
|
};
|
|
43
63
|
});
|
|
@@ -1686,3 +1706,171 @@ describe('getEventsForItemFamilyChanges - conditional eventType', () => {
|
|
|
1686
1706
|
expect(type_conversion_utils_1.TypeConversionUtils.isOutboundCreatableFromEntity).toHaveBeenNthCalledWith(2, undefined, mapFileUtil, colorProjectItem, { item: itemData, assortment });
|
|
1687
1707
|
});
|
|
1688
1708
|
});
|
|
1709
|
+
describe('sendToFlexPLM / handleVibeIQFile / sendPublishPayloadEvent', () => {
|
|
1710
|
+
const config = {
|
|
1711
|
+
taskId: 'task-abc',
|
|
1712
|
+
event: '{"sourceEventId":"src-1"}'
|
|
1713
|
+
};
|
|
1714
|
+
const mapFileUtil = new transform_data_1.MapFileUtil(new sdk_1.Entities());
|
|
1715
|
+
const dc = new data_converter_1.DataConverter(config, mapFileUtil);
|
|
1716
|
+
const events = [{ objectClass: 'LCSProductSeasonLink' }];
|
|
1717
|
+
const eventType = 'ASYNC_PUBLISH_SEASON';
|
|
1718
|
+
beforeEach(() => {
|
|
1719
|
+
createCallArg = undefined;
|
|
1720
|
+
fileUploadCalls = [];
|
|
1721
|
+
fileUploadResult = {
|
|
1722
|
+
id: 'file-123',
|
|
1723
|
+
downloadUrl: 'https://download.url',
|
|
1724
|
+
adminDownloadUrl: 'https://admin.download.url'
|
|
1725
|
+
};
|
|
1726
|
+
});
|
|
1727
|
+
afterEach(() => {
|
|
1728
|
+
jest.restoreAllMocks();
|
|
1729
|
+
});
|
|
1730
|
+
it('should merge outboundPublishEvent into result and call sendPublishPayloadEvent after sendToFlexPLM succeeds', async () => {
|
|
1731
|
+
const bppa = new base_process_publish_assortment_1.BaseProcessPublishAssortment(config, dc, mapFileUtil);
|
|
1732
|
+
const flexResponse = { status: 200, data: { ok: true } };
|
|
1733
|
+
const callOrder = [];
|
|
1734
|
+
const spyFlex = jest.spyOn(flexplm_connect_1.FlexPLMConnect.prototype, 'sendToFlexPLM')
|
|
1735
|
+
.mockImplementation(async () => {
|
|
1736
|
+
await new Promise(r => setTimeout(r, 5));
|
|
1737
|
+
callOrder.push('flex');
|
|
1738
|
+
return flexResponse;
|
|
1739
|
+
});
|
|
1740
|
+
const spyEvent = jest.spyOn(bppa, 'sendPublishPayloadEvent')
|
|
1741
|
+
.mockImplementation(async () => {
|
|
1742
|
+
callOrder.push('event');
|
|
1743
|
+
});
|
|
1744
|
+
const result = await bppa.sendToFlexPLM(events, eventType);
|
|
1745
|
+
expect(spyFlex).toHaveBeenCalledTimes(1);
|
|
1746
|
+
expect(spyEvent).toHaveBeenCalledTimes(1);
|
|
1747
|
+
expect(callOrder).toEqual(['flex', 'event']);
|
|
1748
|
+
const passedOutboundPublishEvent = spyFlex.mock.calls[0][0];
|
|
1749
|
+
expect(passedOutboundPublishEvent.taskId).toBe('task-abc');
|
|
1750
|
+
expect(passedOutboundPublishEvent.eventType).toBe(eventType);
|
|
1751
|
+
expect(passedOutboundPublishEvent.objectClass).toBe('LCSSeason');
|
|
1752
|
+
expect(passedOutboundPublishEvent.events).toBe(events);
|
|
1753
|
+
expect(spyEvent).toHaveBeenCalledWith(passedOutboundPublishEvent);
|
|
1754
|
+
expect(result).toEqual({ ...flexResponse, outboundPublishEvent: passedOutboundPublishEvent });
|
|
1755
|
+
});
|
|
1756
|
+
it('should have sendToFlexPLM throw when flexPLMConnect.sendToFlexPLM fails, without calling sendPublishPayloadEvent', async () => {
|
|
1757
|
+
const bppa = new base_process_publish_assortment_1.BaseProcessPublishAssortment(config, dc, mapFileUtil);
|
|
1758
|
+
const flexError = new Error('flex failed');
|
|
1759
|
+
jest.spyOn(flexplm_connect_1.FlexPLMConnect.prototype, 'sendToFlexPLM').mockRejectedValue(flexError);
|
|
1760
|
+
const spyEvent = jest.spyOn(bppa, 'sendPublishPayloadEvent').mockResolvedValue(undefined);
|
|
1761
|
+
await expect(bppa.sendToFlexPLM(events, eventType)).rejects.toMatchObject({
|
|
1762
|
+
message: 'flex failed',
|
|
1763
|
+
outboundPublishEvent: expect.objectContaining({ taskId: 'task-abc', eventType, objectClass: 'LCSSeason', events }),
|
|
1764
|
+
sendToFlexPLMResult: { error: 'flex failed' }
|
|
1765
|
+
});
|
|
1766
|
+
expect(spyEvent).not.toHaveBeenCalled();
|
|
1767
|
+
expect(flexError.sendPublishPayloadEventResult).toBeUndefined();
|
|
1768
|
+
});
|
|
1769
|
+
it('should have sendToFlexPLM throw when sendPublishPayloadEvent fails, with flex result attached', async () => {
|
|
1770
|
+
const bppa = new base_process_publish_assortment_1.BaseProcessPublishAssortment(config, dc, mapFileUtil);
|
|
1771
|
+
const flexResponse = { status: 200 };
|
|
1772
|
+
const eventError = new Error('event failed');
|
|
1773
|
+
jest.spyOn(flexplm_connect_1.FlexPLMConnect.prototype, 'sendToFlexPLM').mockResolvedValue(flexResponse);
|
|
1774
|
+
jest.spyOn(bppa, 'sendPublishPayloadEvent').mockRejectedValue(eventError);
|
|
1775
|
+
await expect(bppa.sendToFlexPLM(events, eventType)).rejects.toMatchObject({
|
|
1776
|
+
message: 'event failed',
|
|
1777
|
+
outboundPublishEvent: expect.objectContaining({ taskId: 'task-abc', eventType }),
|
|
1778
|
+
sendToFlexPLMResult: flexResponse,
|
|
1779
|
+
sendPublishPayloadEventResult: { error: 'event failed' }
|
|
1780
|
+
});
|
|
1781
|
+
});
|
|
1782
|
+
it('should have handleVibeIQFile (vibeiqfile) throw when flexPLMConnect.sendToFlexPLM fails, without calling sendPublishPayloadEvent', async () => {
|
|
1783
|
+
const bppa = new base_process_publish_assortment_1.BaseProcessPublishAssortment(config, dc, mapFileUtil);
|
|
1784
|
+
const flexError = new Error('flex failed');
|
|
1785
|
+
jest.spyOn(flexplm_connect_1.FlexPLMConnect.prototype, 'sendToFlexPLM').mockRejectedValue(flexError);
|
|
1786
|
+
const spyEvent = jest.spyOn(bppa, 'sendPublishPayloadEvent').mockResolvedValue(undefined);
|
|
1787
|
+
await expect(bppa.handleVibeIQFile(events, eventType, 'vibeiqfile')).rejects.toMatchObject({
|
|
1788
|
+
message: 'flex failed',
|
|
1789
|
+
outboundPublishEvent: expect.objectContaining({ eventsFileId: 'file-123' }),
|
|
1790
|
+
sendToFlexPLMResult: { error: 'flex failed' }
|
|
1791
|
+
});
|
|
1792
|
+
expect(spyEvent).not.toHaveBeenCalled();
|
|
1793
|
+
expect(flexError.sendPublishPayloadEventResult).toBeUndefined();
|
|
1794
|
+
});
|
|
1795
|
+
it('should have handleVibeIQFile (vibeiqfile) throw when sendPublishPayloadEvent fails, with flex result attached', async () => {
|
|
1796
|
+
const bppa = new base_process_publish_assortment_1.BaseProcessPublishAssortment(config, dc, mapFileUtil);
|
|
1797
|
+
const flexResponse = { status: 200 };
|
|
1798
|
+
const eventError = new Error('event failed');
|
|
1799
|
+
jest.spyOn(flexplm_connect_1.FlexPLMConnect.prototype, 'sendToFlexPLM').mockResolvedValue(flexResponse);
|
|
1800
|
+
jest.spyOn(bppa, 'sendPublishPayloadEvent').mockRejectedValue(eventError);
|
|
1801
|
+
await expect(bppa.handleVibeIQFile(events, eventType, 'vibeiqfile')).rejects.toMatchObject({
|
|
1802
|
+
message: 'event failed',
|
|
1803
|
+
outboundPublishEvent: expect.objectContaining({ eventsFileId: 'file-123' }),
|
|
1804
|
+
sendToFlexPLMResult: flexResponse,
|
|
1805
|
+
sendPublishPayloadEventResult: { error: 'event failed' }
|
|
1806
|
+
});
|
|
1807
|
+
});
|
|
1808
|
+
it('should merge outboundPublishEvent into FlexPLM result and call sendPublishPayloadEvent when mode is vibeiqfile', async () => {
|
|
1809
|
+
const bppa = new base_process_publish_assortment_1.BaseProcessPublishAssortment(config, dc, mapFileUtil);
|
|
1810
|
+
const flexResponse = { status: 200, data: { ok: true } };
|
|
1811
|
+
const spyFlex = jest.spyOn(flexplm_connect_1.FlexPLMConnect.prototype, 'sendToFlexPLM')
|
|
1812
|
+
.mockResolvedValue(flexResponse);
|
|
1813
|
+
const spyEvent = jest.spyOn(bppa, 'sendPublishPayloadEvent')
|
|
1814
|
+
.mockResolvedValue(undefined);
|
|
1815
|
+
const result = await bppa.handleVibeIQFile(events, eventType, 'vibeiqfile');
|
|
1816
|
+
expect(fileUploadCalls).toHaveLength(1);
|
|
1817
|
+
expect(fileUploadCalls[0].fileName).toBe('ASYNC_PUBLISH_SEASON-events.json');
|
|
1818
|
+
expect(fileUploadCalls[0].mimeType).toBe('application/json');
|
|
1819
|
+
const passedOutboundPublishEvent = spyFlex.mock.calls[0][0];
|
|
1820
|
+
expect(passedOutboundPublishEvent.taskId).toBe('task-abc');
|
|
1821
|
+
expect(passedOutboundPublishEvent.eventType).toBe(eventType);
|
|
1822
|
+
expect(passedOutboundPublishEvent.objectClass).toBe('LCSSeason');
|
|
1823
|
+
expect(passedOutboundPublishEvent.eventsFileId).toBe('file-123');
|
|
1824
|
+
expect(passedOutboundPublishEvent.eventsDownloadLink).toBe('https://download.url');
|
|
1825
|
+
expect(passedOutboundPublishEvent.eventsAdminDownloadLink).toBe('https://admin.download.url');
|
|
1826
|
+
expect(spyFlex).toHaveBeenCalledTimes(1);
|
|
1827
|
+
expect(spyEvent).toHaveBeenCalledTimes(1);
|
|
1828
|
+
expect(spyEvent).toHaveBeenCalledWith(passedOutboundPublishEvent);
|
|
1829
|
+
expect(result).toEqual({ ...flexResponse, outboundPublishEvent: passedOutboundPublishEvent });
|
|
1830
|
+
});
|
|
1831
|
+
it('should skip FlexPLM but still call sendPublishPayloadEvent when mode is vibeiqfile-dontsendtoflexplm', async () => {
|
|
1832
|
+
const bppa = new base_process_publish_assortment_1.BaseProcessPublishAssortment(config, dc, mapFileUtil);
|
|
1833
|
+
const spyFlex = jest.spyOn(flexplm_connect_1.FlexPLMConnect.prototype, 'sendToFlexPLM')
|
|
1834
|
+
.mockResolvedValue({});
|
|
1835
|
+
const spyEvent = jest.spyOn(bppa, 'sendPublishPayloadEvent')
|
|
1836
|
+
.mockResolvedValue(undefined);
|
|
1837
|
+
const result = await bppa.handleVibeIQFile(events, eventType, 'vibeiqfile-dontsendtoflexplm');
|
|
1838
|
+
expect(spyFlex).not.toHaveBeenCalled();
|
|
1839
|
+
expect(spyEvent).toHaveBeenCalledTimes(1);
|
|
1840
|
+
const passedOutboundPublishEvent = spyEvent.mock.calls[0][0];
|
|
1841
|
+
expect(passedOutboundPublishEvent.eventsFileId).toBe('file-123');
|
|
1842
|
+
expect(result).toEqual({
|
|
1843
|
+
message: 'Successfully Uploaded File.',
|
|
1844
|
+
outboundPublishEvent: passedOutboundPublishEvent
|
|
1845
|
+
});
|
|
1846
|
+
});
|
|
1847
|
+
it('should create an external-event with initialEvent parsed from config', async () => {
|
|
1848
|
+
const bppa = new base_process_publish_assortment_1.BaseProcessPublishAssortment(config, dc, mapFileUtil);
|
|
1849
|
+
const outboundPublishEvent = { foo: 'bar' };
|
|
1850
|
+
await bppa.sendPublishPayloadEvent(outboundPublishEvent);
|
|
1851
|
+
expect(createCallArg).toEqual({
|
|
1852
|
+
entityName: 'external-event',
|
|
1853
|
+
object: {
|
|
1854
|
+
originSystemType: 'VibeIQ',
|
|
1855
|
+
objectClass: 'AssortmentPublishedToFlexPLM',
|
|
1856
|
+
outboundPublishEvent,
|
|
1857
|
+
initialEvent: { sourceEventId: 'src-1' }
|
|
1858
|
+
}
|
|
1859
|
+
});
|
|
1860
|
+
});
|
|
1861
|
+
it('should default initialEvent to {} when config has no event', async () => {
|
|
1862
|
+
const bareConfig = { taskId: 'task-x' };
|
|
1863
|
+
const bppa = new base_process_publish_assortment_1.BaseProcessPublishAssortment(bareConfig, dc, mapFileUtil);
|
|
1864
|
+
const outboundPublishEvent = { hello: 'world' };
|
|
1865
|
+
await bppa.sendPublishPayloadEvent(outboundPublishEvent);
|
|
1866
|
+
expect(createCallArg).toEqual({
|
|
1867
|
+
entityName: 'external-event',
|
|
1868
|
+
object: {
|
|
1869
|
+
originSystemType: 'VibeIQ',
|
|
1870
|
+
objectClass: 'AssortmentPublishedToFlexPLM',
|
|
1871
|
+
outboundPublishEvent,
|
|
1872
|
+
initialEvent: {}
|
|
1873
|
+
}
|
|
1874
|
+
});
|
|
1875
|
+
});
|
|
1876
|
+
});
|