Created
August 11, 2025 16:50
-
-
Save 50-Course/c9e8345f313ff39a2003790d133503fe to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| export class AutomationService { | |
| private readonly logger = new Logger(AutomationService.name); | |
| constructor( | |
| @InjectRepository(User) | |
| private userRepository: Repository<User>, | |
| @InjectRepository(CardLog) | |
| private cardLogRepository: Repository<CardLog>, | |
| ) {} | |
| async addCardToParamount( | |
| input: CardAutomationInput, | |
| ): Promise<AutomationResponse> { | |
| let browser; | |
| let page; | |
| try { | |
| const user = await this.userRepository.findOne({ | |
| where: { id: input.userId }, | |
| }); | |
| if (!user) { | |
| throw new NotFoundException("User not found"); | |
| } | |
| if (!user.paramountEmail || !user.paramountPassword) { | |
| throw new Error("User does not have Paramount+ credentials"); | |
| } | |
| this.logger.log(`Starting card automation for user: ${user.email}`); | |
| browser = await puppeteer.launch({ | |
| headless: false, | |
| args: ["--no-sandbox", "--disable-setuid-sandbox"], | |
| }); | |
| page = await browser.newPage(); | |
| await page.setViewport({ width: 1366, height: 768 }); | |
| // here we choose to navigate to Paramount+ sign-in page directly because we know the URL | |
| await this.retryAction(async () => { | |
| await page.goto("https://www.paramountplus.com/account/signin/", { | |
| waitUntil: "networkidle2", | |
| }); | |
| }, "SIGN IN"); | |
| await this.retryAction(async () => { | |
| await page.waitForSelector('input[type="email"]', { timeout: 50000 }); | |
| await page.type('input[type="email"]', user.paramountEmail); | |
| await page.type('input[type="password"]', user.paramountPassword); | |
| await page.click("button.qt-continuebtn"); | |
| // at signup, if vpn is not on, or location is not US, we hit some errors, we listen to that error | |
| // and rethrow it, | |
| // | |
| // we could otherwise, display a structured format of this error in our API layer (the graphQL response as an ErrorResponse) | |
| try { | |
| const errorSelector = "p.form-message.error"; | |
| await page.waitForSelector(errorSelector, { timeout: 50000 }); | |
| const errorElement = await page.$(errorSelector); | |
| const errorMessage = await page.evaluate( | |
| (el) => el.textContent.trim(), | |
| errorElement, | |
| ); | |
| throw new Error(`Login failed: ${errorMessage}`); | |
| } catch (e) { | |
| if (e.name !== "TimeoutError") { | |
| throw e; | |
| } | |
| } | |
| }, "Login"); | |
| // upon login, we're redirected to a paywalled page, and attempt to select a payment plan from the payment options plans | |
| await this.retryAction(async () => { | |
| await page.waitForNavigation({ waitUntil: "networkidle2" }); | |
| }, "Wait for login and redirect to plan selection"); | |
| // as a decision, we select the cheaper payment option plan, our users' can decide to upgrade in future | |
| await this.retryAction(async () => { | |
| await page.waitForSelector("button.qt-continuebtn:nth-of-type(2)", { | |
| timeout: 10000, | |
| }); | |
| await page.click("button.qt-continuebtn:nth-of-type(2)"); | |
| }, "Select the cheaper plan option"); | |
| await this.retryAction(async () => { | |
| await page.waitForNavigation({ waitUntil: "networkidle2" }); | |
| }, "Wait for redirect to payment page"); | |
| // finally, we would fill in the user's payment credentials, submit and log thi payment entry | |
| await this.retryAction(async () => { | |
| await page.waitForSelector( | |
| 'input[name*="card"], input[placeholder*="card"]', | |
| { timeout: 10000 }, | |
| ); | |
| await page.type( | |
| 'input[name*="card"], input[placeholder*="card"]', | |
| input.cardNumber, | |
| ); | |
| await page.type( | |
| 'input[name*="expiry"], input[placeholder*="MM"]', | |
| input.expiryMonth, | |
| ); | |
| await page.type( | |
| 'input[name*="expiry"], input[placeholder*="YY"]', | |
| input.expiryYear, | |
| ); | |
| await page.type( | |
| 'input[name*="cvv"], input[placeholder*="CVV"]', | |
| input.cvv, | |
| ); | |
| if (input.cardholderName) { | |
| await page.type( | |
| 'input[name*="name"], input[placeholder*="name"]', | |
| input.cardholderName, | |
| ); | |
| } | |
| await page.click( | |
| 'button[type="submit"], button:contains("Save"), button:contains("Add")', | |
| ); | |
| }, "Fill card details"); | |
| await this.retryAction(async () => { | |
| await page.waitForSelector( | |
| '.success, .alert-success, [data-testid="success"]', | |
| { timeout: 15000 }, | |
| ); | |
| }, "Wait for success confirmation"); | |
| await this.logCardOperation( | |
| input.userId, | |
| "ADD_CARD", | |
| "SUCCESS", | |
| undefined, | |
| input.cardNumber.slice(-4), | |
| ); | |
| this.logger.log( | |
| `Card automation completed successfully for user: ${user.email}`, | |
| ); | |
| return { | |
| success: true, | |
| message: "Card added successfully to Paramount+", | |
| }; | |
| } catch (error) { | |
| // at this point, we can assume our operation is failing, we: | |
| // log the error for tracing, and debugging | |
| // | |
| // and return a structured response back to our user | |
| this.logger.error( | |
| `Card automation failed: ${error.message}`, | |
| error.stack, | |
| ); | |
| await this.logCardOperation( | |
| input.userId, | |
| "ADD_CARD", | |
| "FAILED", | |
| error.message, | |
| input.cardNumber.slice(-4), | |
| ); | |
| return { | |
| success: false, | |
| errorMessage: error.message, | |
| }; | |
| } finally { | |
| if (page) await page.close(); | |
| if (browser) await browser.close(); | |
| } | |
| } | |
| // leave every other stuffs the same | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment