diff --git a/index.ts b/index.ts index 3f3ffc0..6ba276f 100644 --- a/index.ts +++ b/index.ts @@ -671,9 +671,18 @@ export enum APIError { * An error that indicates the transaction identifier doesn’t represent a consumable in-app purchase. * * {@link https://developer.apple.com/documentation/appstoreserverapi/invalidtransactionnotconsumableerror InvalidTransactionNotConsumableError} + * + * @deprecated */ INVALID_TRANSACTION_NOT_CONSUMABLE = 4000043, + /** + * An error that indicates the transaction identifier represents an unsupported in-app purchase type. + * + * {@link https://developer.apple.com/documentation/appstoreserverapi/invalidtransactiontypenotsupportederror InvalidTransactionTypeNotSupportedError} + */ + INVALID_TRANSACTION_TYPE_NOT_SUPPORTED = 4000047, + /** * An error that indicates the subscription doesn't qualify for a renewal-date extension due to its subscription state. * diff --git a/models/ConsumptionRequest.ts b/models/ConsumptionRequest.ts index 5eab90d..bfde44a 100644 --- a/models/ConsumptionRequest.ts +++ b/models/ConsumptionRequest.ts @@ -7,6 +7,7 @@ import { LifetimeDollarsPurchased } from "./LifetimeDollarsPurchased" import { LifetimeDollarsRefunded } from "./LifetimeDollarsRefunded" import { Platform } from "./Platform" import { PlayTime } from "./PlayTime" +import { RefundPreference } from "./RefundPreference" import { UserStatus } from "./UserStatus" /** @@ -92,4 +93,11 @@ export interface ConsumptionRequest { * {@link https://developer.apple.com/documentation/appstoreserverapi/userstatus userStatus} **/ userStatus?: UserStatus | number + + /** + * A value that indicates your preference, based on your operational logic, as to whether Apple should grant the refund. + * + * {@link https://developer.apple.com/documentation/appstoreserverapi/refundpreference refundPreference} + **/ + refundPreference?: RefundPreference | number } \ No newline at end of file diff --git a/models/ConsumptionRequestReason.ts b/models/ConsumptionRequestReason.ts new file mode 100644 index 0000000..03a8b8c --- /dev/null +++ b/models/ConsumptionRequestReason.ts @@ -0,0 +1,18 @@ +// Copyright (c) 2024 Apple Inc. Licensed under MIT License. + +import { StringValidator } from "./Validator"; + +/** + * The customer-provided reason for a refund request. + * + * {@link https://developer.apple.com/documentation/appstoreservernotifications/consumptionrequestreason consumptionRequestReason} + */ +export enum ConsumptionRequestReason { + UNINTENDED_PURCHASE = "UNINTENDED_PURCHASE", + FULFILLMENT_ISSUE = "FULFILLMENT_ISSUE", + UNSATISFIED_WITH_PURCHASE = "UNSATISFIED_WITH_PURCHASE", + LEGAL = "LEGAL", + OTHER = "OTHER", +} + +export class ConsumptionRequestReasonValidator extends StringValidator {} \ No newline at end of file diff --git a/models/Data.ts b/models/Data.ts index 440a5ee..4746550 100644 --- a/models/Data.ts +++ b/models/Data.ts @@ -1,5 +1,6 @@ // Copyright (c) 2023 Apple Inc. Licensed under MIT License. +import { ConsumptionRequestReason, ConsumptionRequestReasonValidator } from "./ConsumptionRequestReason" import { Environment, EnvironmentValidator } from "./Environment" import { Status, StatusValidator } from "./Status" import { Validator } from "./Validator" @@ -59,12 +60,20 @@ export interface Data { * {@link https://developer.apple.com/documentation/appstoreservernotifications/status status} **/ status?: Status | number + + /** + * The reason the customer requested the refund. + * + * {@link https://developer.apple.com/documentation/appstoreservernotifications/consumptionrequestreason consumptionRequestReason} + **/ + consumptionRequestReason?: ConsumptionRequestReason | string } export class DataValidator implements Validator { static readonly environmentValidator = new EnvironmentValidator() static readonly statusValidator = new StatusValidator() + static readonly consumptionRequestReasonValidator = new ConsumptionRequestReasonValidator() validate(obj: any): obj is Data { if ((typeof obj['environment'] !== 'undefined') && !(DataValidator.environmentValidator.validate(obj['environment']))) { return false @@ -87,6 +96,9 @@ export class DataValidator implements Validator { if ((typeof obj['status'] !== 'undefined') && !(DataValidator.statusValidator.validate(obj['status']))) { return false } + if ((typeof obj['consumptionRequestReason'] !== 'undefined') && !(DataValidator.consumptionRequestReasonValidator.validate(obj['consumptionRequestReason']))) { + return false + } return true } } diff --git a/models/RefundPreference.ts b/models/RefundPreference.ts new file mode 100644 index 0000000..76d517b --- /dev/null +++ b/models/RefundPreference.ts @@ -0,0 +1,13 @@ +// Copyright (c) 2024 Apple Inc. Licensed under MIT License. + +/** + * A value that indicates your preferred outcome for the refund request. + * + * {@link https://developer.apple.com/documentation/appstoreserverapi/refundpreference refundPreference} + */ +export enum RefundPreference { + UNDECLARED = 0, + PREFER_GRANT = 1, + PREFER_DECLINE = 2, + NO_PREFERENCE = 3, +} diff --git a/tests/resources/models/signedConsumptionRequestNotification.json b/tests/resources/models/signedConsumptionRequestNotification.json new file mode 100644 index 0000000..03c9890 --- /dev/null +++ b/tests/resources/models/signedConsumptionRequestNotification.json @@ -0,0 +1,16 @@ +{ + "notificationType": "CONSUMPTION_REQUEST", + "notificationUUID": "002e14d5-51f5-4503-b5a8-c3a1af68eb20", + "data": { + "environment": "LocalTesting", + "appAppleId": 41234, + "bundleId": "com.example", + "bundleVersion": "1.2.3", + "signedTransactionInfo": "signed_transaction_info_value", + "signedRenewalInfo": "signed_renewal_info_value", + "status": 1, + "consumptionRequestReason": "UNINTENDED_PURCHASE" + }, + "version": "2.0", + "signedDate": 1698148900000 +} \ No newline at end of file diff --git a/tests/unit-tests/api_client.test.ts b/tests/unit-tests/api_client.test.ts index 4b00337..b12ab2b 100644 --- a/tests/unit-tests/api_client.test.ts +++ b/tests/unit-tests/api_client.test.ts @@ -15,6 +15,7 @@ import { Subtype } from "../../models/Subtype"; import { UserStatus } from "../../models/UserStatus"; import { readFile } from "../util" import { InAppOwnershipType } from "../../models/InAppOwnershipType"; +import { RefundPreference } from "../../models/RefundPreference"; import { APIError, APIException, AppStoreServerAPIClient, ExtendReasonCode, ExtendRenewalDateRequest, MassExtendRenewalDateRequest, NotificationHistoryRequest, NotificationHistoryResponseItem, Order, OrderLookupStatus, ProductType, SendAttemptResult, TransactionHistoryRequest } from "../../index"; import { Response } from "node-fetch"; @@ -387,6 +388,7 @@ describe('The api client ', () => { expect(6).toBe(body.lifetimeDollarsRefunded) expect(7).toBe(body.lifetimeDollarsPurchased) expect(4).toBe(body.userStatus) + expect(3).toBe(body.refundPreference) }); const consumptionRequest: ConsumptionRequest = { @@ -400,7 +402,8 @@ describe('The api client ', () => { playTime: PlayTime.ONE_DAY_TO_FOUR_DAYS, lifetimeDollarsRefunded: LifetimeDollarsRefunded.ONE_THOUSAND_DOLLARS_TO_ONE_THOUSAND_NINE_HUNDRED_NINETY_NINE_DOLLARS_AND_NINETY_NINE_CENTS, lifetimeDollarsPurchased: LifetimeDollarsPurchased.TWO_THOUSAND_DOLLARS_OR_GREATER, - userStatus: UserStatus.LIMITED_ACCESS + userStatus: UserStatus.LIMITED_ACCESS, + refundPreference: RefundPreference.NO_PREFERENCE } client.sendConsumptionData("49571273", consumptionRequest); diff --git a/tests/unit-tests/transaction_decoding.test.ts b/tests/unit-tests/transaction_decoding.test.ts index 2a8e85f..a522e79 100644 --- a/tests/unit-tests/transaction_decoding.test.ts +++ b/tests/unit-tests/transaction_decoding.test.ts @@ -13,6 +13,7 @@ import { InAppOwnershipType } from "../../models/InAppOwnershipType"; import { RevocationReason } from "../../models/RevocationReason"; import { TransactionReason } from "../../models/TransactionReason"; import { Type } from "../../models/Type"; +import { ConsumptionRequestReason } from "../../models/ConsumptionRequestReason"; describe('Testing decoding of signed data', () => { @@ -102,6 +103,29 @@ describe('Testing decoding of signed data', () => { expect("signed_transaction_info_value").toBe(notification.data!.signedTransactionInfo) expect("signed_renewal_info_value").toBe(notification.data!.signedRenewalInfo) expect(Status.ACTIVE).toBe(notification.data!.status) + expect(notification.data!.consumptionRequestReason).toBeFalsy() + }) + it('should decode a signed CONSUMPTION_REQUEST notification', async () => { + const signedNotification = createSignedDataFromJson("tests/resources/models/signedConsumptionRequestNotification.json") + + const notification = await getDefaultSignedPayloadVerifier().verifyAndDecodeNotification(signedNotification) + + expect(NotificationTypeV2.CONSUMPTION_REQUEST).toBe(notification.notificationType) + expect(notification.subtype).toBeFalsy() + expect("002e14d5-51f5-4503-b5a8-c3a1af68eb20").toBe(notification.notificationUUID) + expect("2.0").toBe(notification.version) + expect(1698148900000).toBe(notification.signedDate) + expect(notification.data).toBeTruthy() + expect(notification.summary).toBeFalsy() + expect(notification.externalPurchaseToken).toBeFalsy() + expect(Environment.LOCAL_TESTING).toBe(notification.data!.environment) + expect(41234).toBe(notification.data!.appAppleId) + expect("com.example").toBe(notification.data!.bundleId) + expect("1.2.3").toBe(notification.data!.bundleVersion) + expect("signed_transaction_info_value").toBe(notification.data!.signedTransactionInfo) + expect("signed_renewal_info_value").toBe(notification.data!.signedRenewalInfo) + expect(Status.ACTIVE).toBe(notification.data!.status) + expect(ConsumptionRequestReason.UNINTENDED_PURCHASE).toBe(notification.data!.consumptionRequestReason) }) it('should decode a signed summary notification', async () => { const signedNotification = createSignedDataFromJson("tests/resources/models/signedSummaryNotification.json")