Skip to content

Commit

Permalink
Merge pull request #125 from pliancy/feat/price-sheet
Browse files Browse the repository at this point in the history
feat: add support for partner center pricesheet
  • Loading branch information
santese authored Sep 9, 2024
2 parents 1584c3d + 906ea87 commit 4b3a777
Show file tree
Hide file tree
Showing 5 changed files with 177 additions and 96 deletions.
16 changes: 8 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
}
},
"dependencies": {
"axios": "1.7.3",
"axios": "1.7.7",
"jsonwebtoken": "9.0.2",
"tslib": "2.6.3"
},
Expand All @@ -17,18 +17,18 @@
"@pliancy/semantic-release-config-npm": "2.2.0",
"@types/jest": "29.5.12",
"@types/jsonwebtoken": "9.0.6",
"@types/node": "22.2.0",
"@types/node": "22.5.4",
"commitizen": "4.3.0",
"concurrently": "^8.2.2",
"concurrently": "9.0.0",
"cpy-cli": "5.0.0",
"husky": "9.1.4",
"husky": "9.1.5",
"jest": "29.7.0",
"jest-mock-axios": "4.7.3",
"open-cli": "8.0.0",
"pinst": "3.0.0",
"rimraf": "6.0.1",
"ts-jest": "29.2.4",
"typescript": "5.5.4"
"ts-jest": "29.2.5",
"typescript": "5.6.2"
},
"keywords": [
"microsoft-partnercenter-node",
Expand Down Expand Up @@ -62,7 +62,7 @@
},
"version": "5.0.0",
"volta": {
"node": "20.11.1",
"yarn": "1.22.21"
"node": "20.17.0",
"yarn": "1.22.22"
}
}
36 changes: 36 additions & 0 deletions src/lib/microsoft-partnercenter.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { MicrosoftPartnerCenter } from './microsoft-partnercenter'
import mockAxios from 'jest-mock-axios'
import { OrderLineItem } from './types/orders.types'
import { ApplicationConsent } from './types'
import { TokenManager } from './utils/http-token-manager'

describe('Microsoft Partner Center', () => {
let partnerCenter: MicrosoftPartnerCenter
Expand Down Expand Up @@ -232,4 +233,39 @@ describe('Microsoft Partner Center', () => {
expect(result).toEqual({ id: '1' })
expect(mockAxios.get).toHaveBeenCalledWith('/customers/1/users/1')
})

it('should get price sheet', async () => {
// Mock the TokenManager
const mockTokenManager = {
getAccessToken: jest.fn().mockResolvedValue('mock-access-token'),
} as unknown as TokenManager
;(partnerCenter as any).tokenManager = mockTokenManager

const mockBinaryData = Buffer.from('mock price sheet data')

jest.spyOn(mockAxios, 'get').mockResolvedValue({
data: mockBinaryData,
headers: { 'content-type': 'application/octet-stream' },
})

const result = await partnerCenter.getPriceSheet()

expect(Buffer.isBuffer(result)).toBe(true)
expect(result.toString()).toBe('mock price sheet data')

expect(mockAxios.get).toHaveBeenCalledWith(
"https://api.partner.microsoft.com/v1.0/sales/pricesheets(Market='US',PricesheetView='updatedlicensebased')/$value",
{
responseType: 'arraybuffer',
headers: {
Authorization: 'Bearer mock-access-token',
'Accept-Encoding': 'deflate',
},
},
)

expect(mockTokenManager.getAccessToken).toHaveBeenCalledWith(
'https://api.partner.microsoft.com',
)
})
})
24 changes: 24 additions & 0 deletions src/lib/microsoft-partnercenter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -318,4 +318,28 @@ export class MicrosoftPartnerCenter extends MicrosoftApiBase {
const { data } = await this.httpAgent.get(url)
return data.items
}

/**
* Gets price sheet for market, default is US and nce license based
* https://learn.microsoft.com/en-us/partner-center/developer/get-a-price-sheet
* @param market - Market code
* @param priceSheetView - Price sheet view
* @returns Price sheet as a buffer - Which is either a csv or compressed csv
*/
async getPriceSheet(market = 'US', priceSheetView = 'updatedlicensebased'): Promise<Buffer> {
// This api call needs a different resource see: https://github.com/microsoft/Partner-Center-PowerShell/issues/405
const tokenManager = this.tokenManager
const accessToken = await tokenManager.getAccessToken('https://api.partner.microsoft.com')
const { data } = await this.httpAgent.get(
`https://api.partner.microsoft.com/v1.0/sales/pricesheets(Market='${market}',PricesheetView='${priceSheetView}')/$value`,
{
responseType: 'arraybuffer',
headers: {
Authorization: `Bearer ${accessToken}`,
'Accept-Encoding': 'deflate',
},
},
)
return data
}
}
56 changes: 37 additions & 19 deletions src/lib/utils/http-token-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,15 @@ import qs from 'querystring'
import { decode, JwtPayload } from 'jsonwebtoken'
import { GraphApiConfig, IOAuthResponse, IPartnerCenterConfig } from '../types/common.types'

type AuthData = {
grant_type: string
client_id: string
client_secret: string
scope: string
refresh_token?: string
resource?: string
}

export class TokenManager {
private accessToken = ''
private _refreshToken = ''
Expand All @@ -29,9 +38,9 @@ export class TokenManager {
return this._refreshToken
}

async getAccessToken() {
async getAccessToken(resource?: string) {
if (!this.accessToken || this.isTokenExpired()) {
await this.authenticate()
await this.authenticate(resource)
}
return this.accessToken
}
Expand Down Expand Up @@ -61,8 +70,8 @@ export class TokenManager {
throw err
}

private async authenticate() {
let authData = this.prepareAuthData()
private async authenticate(resource?: string) {
let authData = this.prepareAuthData(resource)

try {
const tenantId = this.getTenantId()
Expand All @@ -88,22 +97,31 @@ export class TokenManager {
}
}

private prepareAuthData() {
if (this.config.authentication.refreshToken) {
return qs.stringify({
grant_type: 'refresh_token',
refresh_token: this.config.authentication.refreshToken,
client_id: this.config.authentication.clientId,
client_secret: this.config.authentication.clientSecret,
scope: this.scope,
})
}
return qs.stringify({
grant_type: 'client_credentials',
client_id: this.config.authentication.clientId,
client_secret: this.config.authentication.clientSecret,
private prepareAuthData(resource?: string): string {
const { refreshToken, clientId, clientSecret } = this.config.authentication

const baseAuthData = {
client_id: clientId,
client_secret: clientSecret,
scope: this.scope,
})
}

const authData: AuthData = refreshToken
? {
...baseAuthData,
grant_type: 'refresh_token',
refresh_token: refreshToken,
}
: {
...baseAuthData,
grant_type: 'client_credentials',
}

if (resource) {
authData.resource = resource
}

return qs.stringify(authData)
}
private isTokenExpired() {
if (!this.accessToken) {
Expand Down
Loading

0 comments on commit 4b3a777

Please sign in to comment.