diff --git a/src/api/issuing/cards.js b/src/api/issuing/cards.js index f5ed70d..216c9fa 100644 --- a/src/api/issuing/cards.js +++ b/src/api/issuing/cards.js @@ -86,13 +86,11 @@ export default class Cards { * Only the fields you provide values for are updated. Passing `null` for a field **removes** * its existing value, so omit a field to leave it untouched rather than sending null. * - * Pass headers to request the card's encrypted credentials in the response. Set - * `return-encrypted-cvv` to `true` together with an `Encryption-Key`; supplying the flag - * without the key returns a 422 with error code `encryption_key_required`. - * - * The headers travel on a copy of the config rather than on the body, which is how - * getRequestHeaders picks them up for every verb. Putting them on the body would also send - * them as JSON fields. + * The `headers` parameter (`return-encrypted-cvv`, `Encryption-Key`) is deprecated: the API + * no longer documents these headers, and the response does not carry encrypted + * credentials. When set, they are still sent unchanged, on a copy of the config + * rather than on the body. Use the method without headers; the parameter will be removed in + * the next major version. * * `body.status` (string enum, optional, only `active` allowed) reactivates an * `inactive` or `suspended` card. Mutually exclusive with @@ -105,24 +103,28 @@ export default class Cards { * * The response has the same fields as the get-card response (including * `last_activated_on` and `scheduled_revocation_date`), plus a required - * `last_modified_date`. It no longer includes `encrypted_cvv`. For a virtual card, the + * `last_modified_date`. It does not include `encrypted_cvv`. For a virtual card, the * response may also include `is_single_use` (boolean), specifying whether the card is set * to expire after a single use; physical cards never send it. * * @memberof Cards - * @param {string} id Card id. + * @param {string} id Card id. Pattern `^crd_[a-z0-9]{26}$`, exactly 30 characters. * @param {Object} body Card params to update: `status`, `reference`, `metadata`, - * `expiry_month`, `expiry_year`, `scheduled_revocation_date`, `revocation_date` and - * `scheduled_activation_date`. - * @param {Object} [headers] Optional HTTP headers. - * @param {boolean} [headers.return-encrypted-cvv] Set to `true` to retrieve the card's - * encrypted credentials in the response. Requires `Encryption-Key`. - * @param {string} [headers.Encryption-Key] The RSA public key used to encrypt the returned - * credentials. Required when `return-encrypted-cvv` is `true`. Provide the key with the - * `-----BEGIN PUBLIC KEY-----` and `-----END PUBLIC KEY-----` markers and every newline - * removed, encoded as Base64. - * @return {Promise} A promise to the card update response, carrying - * `last_modified_date`, `_links` and, when the headers requested it, `encrypted_cvv`. + * `expiry_month`, `expiry_year`, `scheduled_revocation_date`, `revocation_date` + * (deprecated, use `scheduled_revocation_date`) and `scheduled_activation_date`. + * @param {Object} [headers] Deprecated. The API no longer documents these headers; use the + * method without headers. Will be removed in the next major version. When set, they are + * still sent unchanged. + * @param {boolean} [headers.return-encrypted-cvv] Deprecated, see `headers`. + * @param {string} [headers.Encryption-Key] Deprecated, see `headers`. + * @return {Promise} A promise to the card update response (update-card-response): + * `id`, `client_id`, `entity_id`, `cardholder_id`, `card_product_id`, `user_id`, + * `last_four`, `expiry_month`, `expiry_year`, `status`, `display_name`, `type`, + * `billing_currency`, `issuing_country`, `reference`, `metadata`, + * `scheduled_revocation_date`, `revocation_date` (deprecated, use + * `scheduled_revocation_date`), `scheduled_activation_date`, `last_activated_on`, + * `root_card_id`, `parent_card_id`, `scheme`, `created_date`, `last_modified_date`, `_links` + * and, for virtual cards only (`type` is `virtual`), `is_single_use`. */ async updateCard(id, body, headers) { try { @@ -283,9 +285,11 @@ export default class Cards { * Revokes an inactive, active, or suspended card to permanently decline all incoming authorizations. * * @memberof Cards - * @param {string} id Card id. - * @param {Object} body Card params. - * @return {Promise} A promise to the card response. + * @param {string} id Card id. Pattern `^crd_[a-z0-9]{26}$`, exactly 30 characters. + * @param {Object} [body] Optional revoke-card request. When omitted, the request is sent + * without a body. + * @param {string} [body.reason] Optional reason: `expired`, `reported_lost` or `reported_stolen`. + * @return {Promise} A promise to the revoke-card response, carrying `_links`. */ async revokeCard(id, body) { try { @@ -306,9 +310,11 @@ export default class Cards { * Suspends an active or inactive card to temporarily decline all incoming authorizations. * * @memberof Cards - * @param {string} id Card id. - * @param {Object} body Card params. - * @return {Promise} A promise to the card response. + * @param {string} id Card id. Pattern `^crd_[a-z0-9]{26}$`, exactly 30 characters. + * @param {Object} [body] Optional suspend-card request. When omitted, the request is sent + * without a body. + * @param {string} [body.reason] Optional reason: `suspected_lost` or `suspected_stolen`. + * @return {Promise} A promise to the suspend-card response, carrying `_links`. */ async suspendCard(id, body) { try { diff --git a/src/api/issuing/issuing.js b/src/api/issuing/issuing.js index 6560e08..ece9184 100644 --- a/src/api/issuing/issuing.js +++ b/src/api/issuing/issuing.js @@ -58,6 +58,52 @@ export default class Issuing { return this.cards.getCardDetails(id); } + /** + * Updates a card you issued previously. + * + * Only the fields you provide values for are updated. Passing `null` for a field **removes** + * its existing value, so omit a field to leave it untouched rather than sending null. + * + * The `headers` parameter (`return-encrypted-cvv`, `Encryption-Key`) is deprecated: the API + * no longer documents these headers, and the response does not carry encrypted + * credentials. When set, they are still sent unchanged, on a copy of the config + * rather than on the body. Use the method without headers; the parameter will be removed in + * the next major version. + * + * `body.status` (string enum, optional, only `active` allowed) reactivates an + * `inactive` or `suspended` card. Mutually exclusive with + * `scheduled_activation_date`; the API returns + * `scheduled_activation_date_conflicts_with_activation` if both are set. + * + * `body.scheduled_revocation_date` (string, `YYYY-MM-DD`, optional) schedules + * the card to be revoked at midnight UTC on that date. Replaces the deprecated + * `revocation_date` field; if both are provided, `scheduled_revocation_date` wins. + * + * The response has the same fields as the get-card response (including + * `last_activated_on` and `scheduled_revocation_date`), plus a required + * `last_modified_date`. It does not include `encrypted_cvv`. For a virtual card, the + * response may also include `is_single_use` (boolean), specifying whether the card is set + * to expire after a single use; physical cards never send it. + * + * @memberof Issuing + * @param {string} id Card id. Pattern `^crd_[a-z0-9]{26}$`, exactly 30 characters. + * @param {Object} body Card params to update: `status`, `reference`, `metadata`, + * `expiry_month`, `expiry_year`, `scheduled_revocation_date`, `revocation_date` + * (deprecated, use `scheduled_revocation_date`) and `scheduled_activation_date`. + * @param {Object} [headers] Deprecated. The API no longer documents these headers; use the + * method without headers. Will be removed in the next major version. When set, they are + * still sent unchanged. + * @param {boolean} [headers.return-encrypted-cvv] Deprecated, see `headers`. + * @param {string} [headers.Encryption-Key] Deprecated, see `headers`. + * @return {Promise} A promise to the card update response (update-card-response): + * `id`, `client_id`, `entity_id`, `cardholder_id`, `card_product_id`, `user_id`, + * `last_four`, `expiry_month`, `expiry_year`, `status`, `display_name`, `type`, + * `billing_currency`, `issuing_country`, `reference`, `metadata`, + * `scheduled_revocation_date`, `revocation_date` (deprecated, use + * `scheduled_revocation_date`), `scheduled_activation_date`, `last_activated_on`, + * `root_card_id`, `parent_card_id`, `scheme`, `created_date`, `last_modified_date`, `_links` + * and, for virtual cards only (`type` is `virtual`), `is_single_use`. + */ async updateCard(id, body, headers) { return this.cards.updateCard(id, body, headers); } @@ -86,10 +132,30 @@ export default class Issuing { return this.cards.renewCard(id, body); } + /** + * Revokes an inactive, active, or suspended card to permanently decline all incoming authorizations. + * + * @memberof Issuing + * @param {string} id Card id. Pattern `^crd_[a-z0-9]{26}$`, exactly 30 characters. + * @param {Object} [body] Optional revoke-card request. When omitted, the request is sent + * without a body. + * @param {string} [body.reason] Optional reason: `expired`, `reported_lost` or `reported_stolen`. + * @return {Promise} A promise to the revoke-card response, carrying `_links`. + */ async revokeCard(id, body) { return this.cards.revokeCard(id, body); } + /** + * Suspends an active or inactive card to temporarily decline all incoming authorizations. + * + * @memberof Issuing + * @param {string} id Card id. Pattern `^crd_[a-z0-9]{26}$`, exactly 30 characters. + * @param {Object} [body] Optional suspend-card request. When omitted, the request is sent + * without a body. + * @param {string} [body.reason] Optional reason: `suspected_lost` or `suspected_stolen`. + * @return {Promise} A promise to the suspend-card response, carrying `_links`. + */ async suspendCard(id, body) { return this.cards.suspendCard(id, body); } diff --git a/test/issuing/cards/cards-it.js b/test/issuing/cards/cards-it.js index 4a7c9b5..b6eae86 100644 --- a/test/issuing/cards/cards-it.js +++ b/test/issuing/cards/cards-it.js @@ -150,22 +150,15 @@ describe.skip('Integration::Issuing::Cards - AuthenticationError: Requires CHECK expect(response._links.self.href).to.contain(card.id); }); - // The 2026-09-17 spec (INT-1700) removed encrypted_cvv from update-card-response entirely, - // so return-encrypted-cvv/Encryption-Key no longer make the update response carry it. This - // test previously asserted the opposite (added by INT-1695, when the field still existed). + // The API no longer documents the update-card headers; the SDK still sends them when set. + it('should update a card when the deprecated headers are set', async () => { + const response = await cko_issuing.issuing.updateCard( + card.id, + { reference: 'X-123456-N11' }, + { 'return-encrypted-cvv': true } + ); - // The flag without the key is a documented 422 carrying encryption_key_required. - it('should throw ValidationError when requesting the cvv without an encryption key', async () => { - try { - await cko_issuing.issuing.updateCard( - card.id, - { reference: 'X-123456-N11' }, - { 'return-encrypted-cvv': true } - ); - expect.fail('Should have thrown ValidationError'); - } catch (err) { - expect(err).to.be.instanceOf(ValidationError); - expect(err.body.error_codes).to.contain('encryption_key_required'); - } + expect(response.last_modified_date).to.not.be.null; + expect(response._links.self.href).to.contain(card.id); }); }); diff --git a/test/issuing/cards/cards-unit.js b/test/issuing/cards/cards-unit.js index aa7385d..c512c81 100644 --- a/test/issuing/cards/cards-unit.js +++ b/test/issuing/cards/cards-unit.js @@ -1,7 +1,7 @@ import nock from "nock"; import { expect } from "chai"; import Checkout from "../../../src/Checkout.js"; -import { AuthenticationError, NotFoundError, ValidationError } from "../../../src/services/errors.js"; +import { AuthenticationError, NotFoundError } from "../../../src/services/errors.js"; describe('Unit::Issuing::Cards', () => { it('should create card', async () => { @@ -221,7 +221,7 @@ describe('Unit::Issuing::Cards', () => { .patch('/issuing/cards/crd_fa6psq242dcd6fdn5gifcq1491') .reply(200, { id: "crd_fa6psq242dcd6fdn5gifcq1491", - status: "inactive", + status: "active", reference: "X-123456-N11-UPDATED", scheduled_revocation_date: "2026-12-31", last_modified_date: "2021-09-09T19:41:39Z", @@ -251,13 +251,13 @@ describe('Unit::Issuing::Cards', () => { }); const cardResponse = await cko.issuing.updateCard('crd_fa6psq242dcd6fdn5gifcq1491', { - status: "inactive", + status: "active", reference: "X-123456-N11-UPDATED", scheduled_revocation_date: "2026-12-31" }); expect(cardResponse.id).to.equal("crd_fa6psq242dcd6fdn5gifcq1491"); - expect(cardResponse.status).to.equal("inactive"); + expect(cardResponse.status).to.equal("active"); expect(cardResponse.reference).to.equal("X-123456-N11-UPDATED"); expect(cardResponse.scheduled_revocation_date).to.equal("2026-12-31"); expect(cardResponse.last_modified_date).to.equal("2021-09-09T19:41:39Z"); @@ -336,7 +336,7 @@ describe('Unit::Issuing::Cards', () => { }); try { - await cko.issuing.updateCard('not_found', { status: "inactive" }); + await cko.issuing.updateCard('not_found', { status: "active" }); } catch (err) { expect(err).to.be.instanceOf(NotFoundError); } @@ -861,14 +861,13 @@ describe('Unit::Issuing::Cards', () => { } }); - // The return-encrypted-cvv and Encryption-Key headers on PATCH /issuing/cards/{cardId}. + // The deprecated return-encrypted-cvv and Encryption-Key headers on PATCH /issuing/cards/{cardId}. // - // These assert the headers on the outgoing request, because the spec spells them case - // sensitively (return-encrypted-cvv lower case, Encryption-Key title case) and because they - // travel on a copy of the config rather than on the body. The 2026-09-17 spec (INT-1700) - // removed encrypted_cvv from update-card-response entirely, so the response body is - // asserted without it regardless of whether these headers are sent. - describe('update headers', () => { + // The API no longer documents these headers. The SDK still sends them + // unchanged when the caller sets them, on a copy of the config rather than on the body, so + // these tests assert the outgoing request. The response is the plain update-card-response, + // whether or not the headers are sent. + describe('deprecated update headers', () => { const SUBDOMAIN = "123456789"; const ACCESS_BASE = `https://${SUBDOMAIN}.access.sandbox.checkout.com`; const BASE = `https://${SUBDOMAIN}.api.sandbox.checkout.com`; @@ -895,7 +894,7 @@ describe('Unit::Issuing::Cards', () => { nock.cleanAll(); }); - it('should send both headers with their exact swagger spelling', async () => { + it('should still send both deprecated headers when set', async () => { let seen; nock(BASE) .patch(`/issuing/cards/${CARD_ID}`) @@ -923,9 +922,8 @@ describe('Unit::Issuing::Cards', () => { expect(seen['return-encrypted-cvv']).to.equal('true'); expect(seen['encryption-key']).to.equal('MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A'); - // last_modified_date is required on update-card-response. The 2026-09-17 spec - // (INT-1700) removed encrypted_cvv entirely, so these headers no longer make the - // response carry it; _links is now the same CardLinks shape get-card-response uses. + // last_modified_date is required on update-card-response, which does not include + // encrypted_cvv; _links uses the same CardLinks shape get-card-response uses. expect(result.last_modified_date).to.equal('2026-06-01T10:00:00Z'); expect(result.encrypted_cvv).to.be.undefined; expect(result._links.self.href).to.contain(`/issuing/cards/${CARD_ID}`); @@ -933,8 +931,8 @@ describe('Unit::Issuing::Cards', () => { expect(result._links.self.types).to.deep.equal(['application/json']); }); - // The 2026-09-23 spec update split update-card-response into a virtual/physical - // discriminator; the virtual variant adds is_single_use. + // update-card-response has a virtual/physical discriminator; the virtual variant + // adds is_single_use. it('should round-trip is_single_use for a virtual card response', async () => { nock(BASE) .patch(`/issuing/cards/${CARD_ID}`) @@ -1014,25 +1012,17 @@ describe('Unit::Issuing::Cards', () => { expect(seen['return-encrypted-cvv']).to.equal('true'); }); - // The API answers 422 with error code encryption_key_required when the flag is set without - // a key. The SDK must surface the error body rather than swallowing it. - it('should surface the 422 encryption_key_required error', async () => { + // The API no longer documents these headers; when they are still sent, the call gets + // the normal update-card-response back. + it('should return the update card response when the deprecated headers are set', async () => { nock(BASE) .patch(`/issuing/cards/${CARD_ID}`) - .reply(422, { - request_id: '0HLHPN8802NUF:00000003', - error_type: 'request_invalid', - error_codes: ['encryption_key_required'] - }); + .reply(200, { id: CARD_ID, last_modified_date: '2026-06-01T10:00:00Z' }); - try { - await client().issuing.cards.updateCard(CARD_ID, {}, { 'return-encrypted-cvv': true }); - throw new Error('expected the update to reject'); - } catch (error) { - expect(error).to.be.instanceOf(ValidationError); - expect(error.http_code).to.equal(422); - expect(error.body.error_codes).to.contain('encryption_key_required'); - } + const result = await client().issuing.cards.updateCard(CARD_ID, {}, { 'return-encrypted-cvv': true }); + + expect(result.id).to.equal(CARD_ID); + expect(result.last_modified_date).to.equal('2026-06-01T10:00:00Z'); }); // The renamed field is a plain passthrough here, since node models no card fields, @@ -1055,5 +1045,35 @@ describe('Unit::Issuing::Cards', () => { expect(body.revocation_date).to.equal('2027-03-12'); expect(body.activation_date).to.be.undefined; }); + it('should revoke a card without a body', async () => { + let body = 'unset'; + nock(BASE) + .post(`/issuing/cards/${CARD_ID}/revoke`, (received) => { + body = received; + return true; + }) + .reply(200, { _links: { self: { href: `https://${SUBDOMAIN}.api.checkout.com/issuing/cards/${CARD_ID}` } } }); + + const result = await client().issuing.cards.revokeCard(CARD_ID); + + // No body is sent on the wire when the request is omitted. + expect(body).to.equal(''); + expect(result._links.self.href).to.contain(CARD_ID); + }); + + it('should suspend a card without a body', async () => { + let body = 'unset'; + nock(BASE) + .post(`/issuing/cards/${CARD_ID}/suspend`, (received) => { + body = received; + return true; + }) + .reply(200, { _links: { self: { href: `https://${SUBDOMAIN}.api.checkout.com/issuing/cards/${CARD_ID}` } } }); + + const result = await client().issuing.suspendCard(CARD_ID); + + expect(body).to.equal(''); + expect(result._links.self.href).to.contain(CARD_ID); + }); }); }); diff --git a/types/dist/api/issuing/cards.d.ts b/types/dist/api/issuing/cards.d.ts index 68da8f6..2337c1b 100644 --- a/types/dist/api/issuing/cards.d.ts +++ b/types/dist/api/issuing/cards.d.ts @@ -11,10 +11,14 @@ export default class Cards { * Only the fields you provide values for are updated. Passing `null` for a field removes its * existing value. * - * Pass `headers` to request the encrypted credentials: set `return-encrypted-cvv` to `true` - * together with an `Encryption-Key`. The key must have its `-----BEGIN PUBLIC KEY-----` and - * `-----END PUBLIC KEY-----` markers and every newline removed, encoded as Base64. Supplying - * the flag without the key returns a 422 with error code `encryption_key_required`. + * The response is the update-card-response; it does not include `encrypted_cvv`. + * For virtual cards only (`type` is `virtual`), it may also include `is_single_use`. + */ + updateCard(id: string, body: object): Promise; + /** + * @deprecated The API no longer documents these headers (`return-encrypted-cvv`, + * `Encryption-Key`); use the method without headers. Will be removed in the next major + * version. When set, the headers are still sent unchanged. */ updateCard(id: string, body: object, headers?: object): Promise; enrollThreeDS(id: string, body: object): Promise; @@ -23,6 +27,6 @@ export default class Cards { activateCard(id: string): Promise; getCardCredentials(id: string, body: object): Promise; renewCard(id: string, body: object): Promise; - revokeCard(id: string, body: object): Promise; - suspendCard(id: string, body: object): Promise; + revokeCard(id: string, body?: object): Promise; + suspendCard(id: string, body?: object): Promise; } diff --git a/types/dist/api/issuing/issuing.d.ts b/types/dist/api/issuing/issuing.d.ts index 0778615..694c20d 100644 --- a/types/dist/api/issuing/issuing.d.ts +++ b/types/dist/api/issuing/issuing.d.ts @@ -40,10 +40,14 @@ export default class Issuing { * Only the fields you provide values for are updated. Passing `null` for a field removes its * existing value. * - * Pass `headers` to request the encrypted credentials: set `return-encrypted-cvv` to `true` - * together with an `Encryption-Key`. The key must have its `-----BEGIN PUBLIC KEY-----` and - * `-----END PUBLIC KEY-----` markers and every newline removed, encoded as Base64. Supplying - * the flag without the key returns a 422 with error code `encryption_key_required`. + * The response is the update-card-response; it does not include `encrypted_cvv`. + * For virtual cards only (`type` is `virtual`), it may also include `is_single_use`. + */ + updateCard(id: string, body: object): Promise; + /** + * @deprecated The API no longer documents these headers (`return-encrypted-cvv`, + * `Encryption-Key`); use the method without headers. Will be removed in the next major + * version. When set, the headers are still sent unchanged. */ updateCard(id: string, body: object, headers?: object): Promise; enrollThreeDS(id: string, body: object): Promise; @@ -52,8 +56,8 @@ export default class Issuing { activateCard(id: string): Promise; getCardCredentials(id: string, body: object): Promise; renewCard(id: string, body: object): Promise; - revokeCard(id: string, body: object): Promise; - suspendCard(id: string, body: object): Promise; + revokeCard(id: string, body?: object): Promise; + suspendCard(id: string, body?: object): Promise; // Backwards compatibility - Controls createCardControl(body: object): Promise;