Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down
8 changes: 8 additions & 0 deletions models/ConsumptionRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"

/**
Expand Down Expand Up @@ -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
}
18 changes: 18 additions & 0 deletions models/ConsumptionRequestReason.ts
Original file line number Diff line number Diff line change
@@ -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 {}
12 changes: 12 additions & 0 deletions models/Data.ts
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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<Data> {
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
Expand All @@ -87,6 +96,9 @@ export class DataValidator implements Validator<Data> {
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
}
}
13 changes: 13 additions & 0 deletions models/RefundPreference.ts
Original file line number Diff line number Diff line change
@@ -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,
}
16 changes: 16 additions & 0 deletions tests/resources/models/signedConsumptionRequestNotification.json
Original file line number Diff line number Diff line change
@@ -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
}
5 changes: 4 additions & 1 deletion tests/unit-tests/api_client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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 = {
Expand All @@ -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);
Expand Down
24 changes: 24 additions & 0 deletions tests/unit-tests/transaction_decoding.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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")
Expand Down