Created
April 17, 2026 11:13
-
-
Save peteonrails/ce92cd9ad7487cc5fb8cb68fe2a52129 to your computer and use it in GitHub Desktop.
ENG1S-712: Test artifacts — seed task, Playwright scripts, before/after proof
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
| /home/pete/.local/share/mise/installs/ruby/3.4.7/lib/ruby/3.4.0/bundled_gems.rb:82: warning: ⛔️ WARNING: Sidekiq testing API enabled, but this is not the test environment. Your jobs will not go to Redis. | |
| ⛔️ `require "sidekiq/testing"` is deprecated and will be removed in Sidekiq 9.0. See https://sidekiq.org/wiki/Testing#new-api | |
| ====================================================================== | |
| ENG1S-712: Before/After Proof — SubscriptionSeats::Assign | |
| ====================================================================== | |
| --- BEFORE (main's code) --- | |
| User: 32 | Subscription: active | Membership: 38 | |
| ReplaceIndividualWithAgencyJob enqueued: 0 | |
| RESULT: NO JOB — subscription keeps running until period end | |
| [1;34mINFO [0m 2026-04-16T23:42:19.203Z pid=3703396 tid=27foc: Sidekiq 8.1.2 connecting to Redis with options {size: 5, pool_name: "internal", url: "redis://localhost:6379/"} | |
| --- AFTER (our fix) --- | |
| User: 33 | Subscription: active | Membership: 39 | |
| Assign result: success | |
| ReplaceIndividualWithAgencyJob enqueued: 1 | |
| Job args: [33] | |
| RESULT: JOB FIRED — subscription will be cancelled and refunded | |
| ====================================================================== |
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
| # ENG1S-712 Before/After Proof | |
| # Exercises SubscriptionSeats::Assign with and without the fix, | |
| # showing that ReplaceIndividualWithAgencyJob is only enqueued WITH the fix. | |
| # | |
| # Usage: bin/rails runner /tmp/eng1s712-before-after.rb | |
| require "sidekiq/testing" | |
| agency = Agency.find_by!(name: "Duck Travel Worldwide") | |
| sub = agency.subscription | |
| puts "=" * 70 | |
| puts "ENG1S-712: Before/After Proof — SubscriptionSeats::Assign" | |
| puts "=" * 70 | |
| # --- Setup a fresh user for each test --- | |
| def setup_test_user(agency, sub, label) | |
| email = "eng1s712-#{label}@proof.test" | |
| User.find_by(email:)&.destroy | |
| user = User.create!( | |
| email:, | |
| first_name: label.capitalize, | |
| last_name: "Proof", | |
| password: "abc12345!1!1", | |
| password_confirmation: "abc12345!1!1", | |
| signup_type: "travel_advisor", | |
| signup_status: "active", | |
| agreed_to_terms_of_service: true, | |
| member_of_host_agency: false, | |
| member_of_consortium: false, | |
| confirmed_at: Time.current | |
| ) | |
| user.create_subscription!( | |
| status: "active", | |
| stripe_subscription_id: "sub_proof_#{label}", | |
| current_period_end_at: 30.days.from_now | |
| ) | |
| membership = agency.memberships.create!(user:, role: "agent", status: "active") | |
| seat = sub.subscription_seats.create! | |
| [user, membership, seat] | |
| end | |
| # ------------------------------------------------------- | |
| # BEFORE: main's version (no subscription cancellation) | |
| # ------------------------------------------------------- | |
| user_before, membership_before, seat_before = setup_test_user(agency, sub, "before") | |
| puts "\n--- BEFORE (main's code) ---" | |
| puts "User: #{user_before.id} | Subscription: active | Membership: #{membership_before.id}" | |
| Sidekiq::Testing.fake! do | |
| Sidekiq::Worker.clear_all | |
| # Simulate main's Assign — just assigns the seat, no cancellation check | |
| seat_before.update!(agency_membership: membership_before) | |
| # That's all main does — no cancel_personal_subscription_if_needed call | |
| jobs = Sidekiq::Queues["less_than_five_minutes"].select { |j| j["class"] == "Subscriptions::ReplaceIndividualWithAgencyJob" } | |
| puts "ReplaceIndividualWithAgencyJob enqueued: #{jobs.count}" | |
| puts "RESULT: #{jobs.count > 0 ? 'JOB FIRED (unexpected)' : 'NO JOB — subscription keeps running until period end'}" | |
| end | |
| # Cleanup | |
| seat_before.update!(agency_membership: nil) | |
| membership_before.destroy | |
| user_before.subscription.destroy | |
| user_before.destroy | |
| seat_before.destroy | |
| # ------------------------------------------------------- | |
| # AFTER: our fix (with subscription cancellation) | |
| # ------------------------------------------------------- | |
| user_after, membership_after, seat_after = setup_test_user(agency, sub, "after") | |
| puts "\n--- AFTER (our fix) ---" | |
| puts "User: #{user_after.id} | Subscription: active | Membership: #{membership_after.id}" | |
| Sidekiq::Testing.fake! do | |
| Sidekiq::Worker.clear_all | |
| # Use our service which includes cancel_personal_subscription_if_needed | |
| result = AgencyMemberships::SubscriptionSeats::Assign.call( | |
| membership: membership_after, | |
| subscription_seat: seat_after | |
| ) | |
| puts "Assign result: #{result.success? ? 'success' : 'failure'}" | |
| jobs = Sidekiq::Queues["less_than_five_minutes"].select { |j| j["class"] == "Subscriptions::ReplaceIndividualWithAgencyJob" } | |
| puts "ReplaceIndividualWithAgencyJob enqueued: #{jobs.count}" | |
| if jobs.any? | |
| puts "Job args: #{jobs.first['args'].inspect}" | |
| puts "RESULT: JOB FIRED — subscription will be cancelled and refunded" | |
| else | |
| puts "RESULT: NO JOB (unexpected)" | |
| end | |
| end | |
| # Cleanup | |
| seat_after.update!(agency_membership: nil) | |
| membership_after.destroy | |
| user_after.subscription.destroy | |
| user_after.destroy | |
| seat_after.destroy | |
| puts "\n" + "=" * 70 |
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
| // ENG1S-712 Path A: Manual seat assignment via Team & Billing. | |
| // Proves SubscriptionSeats::Assign fires ReplaceIndividualWithAgencyJob. | |
| const { chromium } = require("playwright") | |
| const path = require("path") | |
| const BASE = process.env.TERN_URL || "http://localhost:3000" | |
| const DIR = "/tmp/eng1s712-screenshots" | |
| async function shoot(page, name) { | |
| const file = path.join(DIR, name + ".png") | |
| await page.screenshot({ path: file, fullPage: true }) | |
| console.log(" screenshot:", file) | |
| } | |
| async function login(page, email, password) { | |
| await page.goto(BASE + "/session/new") | |
| await page.fill("#email", email) | |
| await page.fill("#password", password) | |
| await page.getByRole("button", { name: "Sign In", exact: true }).click() | |
| await page.waitForURL((url) => !url.pathname.startsWith("/session")) | |
| } | |
| async function run() { | |
| const browser = await chromium.launch({ headless: true }) | |
| const page = await browser.newContext({ viewport: { width: 1400, height: 900 } }).then(c => c.newPage()) | |
| try { | |
| console.log("PATH A: Manual Seat Assignment") | |
| console.log(" 1. Login as agency owner") | |
| await login(page, "john@ducktravel.com", "abc12345!1!1") | |
| console.log(" 2. Navigate to Team & Billing") | |
| await page.goto(BASE + "/agencies/1/team") | |
| await page.waitForLoadState("networkidle") | |
| await shoot(page, "path-a-01-before") | |
| console.log(" 3. Open more-actions menu for PathA Advisor") | |
| const row = page.locator(".table-row", { hasText: "eng1s712-path-a@example.com" }) | |
| await row.waitFor({ state: "visible", timeout: 10000 }) | |
| // Click the three-dot menu button scoped to this row | |
| const menuBtn = row.locator("[aria-haspopup='menu']") | |
| await menuBtn.click() | |
| // Wait for a visible dropdown, then click Assign paid seat | |
| await page.waitForTimeout(500) // let dropdown animation finish | |
| await shoot(page, "path-a-02-menu-open") | |
| console.log(" 4. Click 'Assign paid seat'") | |
| // Menu items with method: :post render as button_to forms with role="menuitem" | |
| await page.evaluate(() => { | |
| const items = [...document.querySelectorAll("[role='menuitem']")] | |
| const assign = items.find(el => el.textContent.includes("Assign paid seat") && !el.closest(".hidden")) | |
| if (assign) assign.click() | |
| else throw new Error("Could not find 'Assign paid seat' menu item") | |
| }) | |
| await page.waitForTimeout(1500) | |
| await shoot(page, "path-a-03-after-click") | |
| console.log(" 5. Reload to confirm state change") | |
| await page.goto(BASE + "/agencies/1/team") | |
| await page.waitForLoadState("networkidle") | |
| await shoot(page, "path-a-04-after-reload") | |
| console.log(" 6. Check Sidekiq") | |
| await page.goto(BASE + "/admin/sidekiq") | |
| await page.waitForLoadState("networkidle") | |
| await shoot(page, "path-a-05-sidekiq") | |
| console.log("PATH A: SUCCESS") | |
| } catch (err) { | |
| await shoot(page, "path-a-99-error") | |
| console.error("PATH A FAILED:", err.message) | |
| process.exitCode = 1 | |
| } finally { | |
| await browser.close() | |
| } | |
| } | |
| run() |
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
| // ENG1S-712 Path B: Subagency seat transfer via parent agency invitation. | |
| // Proves transfer_memberships! fires ReplaceIndividualWithAgencyJob for | |
| // transferred members with active personal subscriptions. | |
| const { chromium } = require("playwright") | |
| const path = require("path") | |
| const { execSync } = require("child_process") | |
| const BASE = process.env.TERN_URL || "http://localhost:3000" | |
| const DIR = "/tmp/eng1s712-screenshots" | |
| async function shoot(page, name) { | |
| const file = path.join(DIR, name + ".png") | |
| await page.screenshot({ path: file, fullPage: true }) | |
| console.log(" screenshot:", file) | |
| } | |
| async function login(page, email, password) { | |
| await page.goto(BASE + "/session/new") | |
| await page.fill("#email", email) | |
| await page.fill("#password", password) | |
| await page.getByRole("button", { name: "Sign In", exact: true }).click() | |
| await page.waitForURL((url) => !url.pathname.startsWith("/session")) | |
| } | |
| function getInvitationToken() { | |
| const cmd = `cd /home/pete/workspace/tern/.worktrees/eng1s-712-fix-agency-conversion-refunds/rails && eval "$(mise activate bash)" && bin/rails runner "inv = ParentAgencyInvitation.find_by(email: 'eng1s712-path-b-owner@example.com', status: 'pending'); puts inv&.to_sgid(purpose: :parent_agency_invitation)&.to_s || 'NONE'" 2>/dev/null | tail -1` | |
| return execSync(cmd, { encoding: "utf-8" }).trim() | |
| } | |
| async function run() { | |
| const browser = await chromium.launch({ headless: true }) | |
| const page = await browser.newContext({ viewport: { width: 1400, height: 900 } }).then(c => c.newPage()) | |
| try { | |
| console.log("PATH B: Subagency Seat Transfer") | |
| console.log(" 1. Login as subagency owner") | |
| await login(page, "eng1s712-path-b-owner@example.com", "abc12345!1!1") | |
| console.log(" 2. Visit invitation URL") | |
| const token = getInvitationToken() | |
| if (!token || token === "NONE") throw new Error("No pending invitation found") | |
| // The invitation URL redirects to /users/:id/agency_memberships with the | |
| // acceptance modal auto-opened | |
| await page.goto(BASE + "/parent_agency_invitations/" + token) | |
| await page.waitForLoadState("networkidle") | |
| await page.waitForTimeout(2000) // let modal render | |
| await shoot(page, "path-b-01-invitation-page") | |
| console.log(" 3. Select 'Affiliate existing agency' and choose acting agency") | |
| // Radio button for "existing" should be present if the user has admin agencies | |
| const existingRadio = page.locator("input[value='existing']") | |
| if (await existingRadio.count() > 0) { | |
| await existingRadio.click() | |
| await page.waitForTimeout(500) | |
| } | |
| // Select the acting agency from the dropdown | |
| const agencySelect = page.locator("select[name*='agency_id']") | |
| if (await agencySelect.count() > 0) { | |
| // Find the option with the acting agency name | |
| await agencySelect.selectOption({ label: "ENG1S-712 Acting Agency" }) | |
| await page.waitForTimeout(500) | |
| } | |
| await shoot(page, "path-b-02-form-filled") | |
| console.log(" 4. Submit acceptance") | |
| // The submit button text comes from t(".continue") | |
| const submitBtn = page.getByRole("button", { name: /continue/i }).first() | |
| await submitBtn.click() | |
| // Wait for turbo redirect to the new team page | |
| await page.waitForTimeout(3000) | |
| await page.waitForLoadState("networkidle") | |
| await shoot(page, "path-b-03-after-acceptance") | |
| console.log(" 5. Check Sidekiq") | |
| await page.goto(BASE + "/admin/sidekiq") | |
| await page.waitForLoadState("networkidle") | |
| await shoot(page, "path-b-04-sidekiq") | |
| console.log("PATH B: SUCCESS") | |
| } catch (err) { | |
| await shoot(page, "path-b-99-error") | |
| console.error("PATH B FAILED:", err.message) | |
| process.exitCode = 1 | |
| } finally { | |
| await browser.close() | |
| } | |
| } | |
| run() |
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
| require_relative "../../data_migration" | |
| # Seed data for manual testing of ENG1S-712. | |
| # Creates two test scenarios and prints step-by-step instructions. | |
| # | |
| # Usage: bin/rails eng1s_712:seed | |
| # | |
| # To reset and re-run: bin/rails eng1s_712:reset eng1s_712:seed | |
| namespace :eng1s_712 do | |
| PASSWORD = ENV["SEED_USER_PASSWORD"] || "abc12345!1!1" | |
| desc "Seed test data for ENG1S-712 manual testing" | |
| task seed: :environment do | |
| puts "\n=== ENG1S-712: Seeding test data ===\n\n" | |
| # --- Shared: host agency with subscription + available seats --- | |
| agency = Agency.find_by(name: "Duck Travel Worldwide") | |
| raise "Run db:seed first — Duck Travel Worldwide must exist" unless agency | |
| raise "Duck Travel needs an active subscription" unless agency.subscription&.active? | |
| owner = User.find_by!(email: "john@ducktravel.com") | |
| # ---------------------------------------------------------------- | |
| # PATH A: Manual seat assignment (SubscriptionSeats::Assign) | |
| # ---------------------------------------------------------------- | |
| path_a_user = User.find_or_create_by!(email: "eng1s712-path-a@example.com") do |u| | |
| u.first_name = "PathA" | |
| u.last_name = "Advisor" | |
| u.password = PASSWORD | |
| u.password_confirmation = PASSWORD | |
| u.signup_type = "travel_advisor" | |
| u.signup_status = "active" | |
| u.agreed_to_terms_of_service = true | |
| u.member_of_host_agency = false | |
| u.member_of_consortium = false | |
| u.confirmed_at = Time.current | |
| end | |
| path_a_user.authentication_settings.update!(prompted_for_two_factor: true) | |
| # Active personal subscription (fake Stripe ID — job will fail gracefully) | |
| path_a_user.subscription&.destroy | |
| path_a_user.create_subscription!( | |
| status: "active", | |
| stripe_subscription_id: "sub_eng1s712_path_a_test", | |
| current_period_end_at: 30.days.from_now | |
| ) | |
| # Active unpaid membership | |
| path_a_membership = agency.memberships.find_or_create_by!(user: path_a_user) do |m| | |
| m.role = "agent" | |
| m.status = "active" | |
| end | |
| path_a_membership.update!(status: "active", role: "agent") | |
| path_a_membership.subscription_seats.each { |s| s.update!(agency_membership: nil) } | |
| # Ensure there are at least 2 unoccupied seats (Path A needs 1, Path B needs 1) | |
| sub = agency.subscription | |
| while sub.subscription_seats.unoccupied.count < 2 | |
| sub.subscription_seats.create! | |
| sub.update!(seats: sub.subscription_seats.count) | |
| end | |
| # ---------------------------------------------------------------- | |
| # PATH B: Subagency seat transfer (ParentAgencyInvitations::Accept) | |
| # ---------------------------------------------------------------- | |
| # "Acting agency" — the subagency being absorbed into Duck Travel | |
| acting_agency = Agency.find_by(name: "ENG1S-712 Acting Agency") | |
| unless acting_agency | |
| acting_agency = Agency.new( | |
| name: "ENG1S-712 Acting Agency", | |
| default_billing: "agency_pays", | |
| estimated_advisor_count: 3, | |
| website: "https://eng1s712-acting.example.com" | |
| ) | |
| acting_agency.accreditations.build(value: "712712", agency_accreditation_type: "IATA") | |
| acting_agency.save! | |
| end | |
| # Acting agency subscription with a paid seat | |
| acting_sub = acting_agency.subscription || AgencySubscription.create!( | |
| agency: acting_agency, | |
| amount_cents: 3900, | |
| amount_currency: "USD", | |
| cancel_at_period_end: false, | |
| current_period_end_at: 1.month.from_now, | |
| interval: "month", | |
| seats: 2, | |
| status: "active" | |
| ) | |
| acting_sub.update!(status: "active") unless acting_sub.active? | |
| # Subagency owner — will accept the invitation | |
| path_b_owner = User.find_or_create_by!(email: "eng1s712-path-b-owner@example.com") do |u| | |
| u.first_name = "PathB" | |
| u.last_name = "Owner" | |
| u.password = PASSWORD | |
| u.password_confirmation = PASSWORD | |
| u.signup_type = "admin" | |
| u.signup_status = "active" | |
| u.agreed_to_terms_of_service = true | |
| u.member_of_host_agency = false | |
| u.member_of_consortium = false | |
| u.confirmed_at = Time.current | |
| end | |
| path_b_owner.authentication_settings.update!(prompted_for_two_factor: true) | |
| # Owner membership in acting agency | |
| acting_agency.memberships.find_or_create_by!(user: path_b_owner) do |m| | |
| m.role = "owner" | |
| m.status = "active" | |
| end | |
| # Transferred member — has personal subscription + paid seat in acting agency | |
| path_b_member = User.find_or_create_by!(email: "eng1s712-path-b-member@example.com") do |u| | |
| u.first_name = "PathB" | |
| u.last_name = "PaidMember" | |
| u.password = PASSWORD | |
| u.password_confirmation = PASSWORD | |
| u.signup_type = "travel_advisor" | |
| u.signup_status = "active" | |
| u.agreed_to_terms_of_service = true | |
| u.member_of_host_agency = false | |
| u.member_of_consortium = false | |
| u.confirmed_at = Time.current | |
| end | |
| path_b_member.authentication_settings.update!(prompted_for_two_factor: true) | |
| # Active personal subscription on the transferred member | |
| path_b_member.subscription&.destroy | |
| path_b_member.create_subscription!( | |
| status: "active", | |
| stripe_subscription_id: "sub_eng1s712_path_b_test", | |
| current_period_end_at: 30.days.from_now | |
| ) | |
| # Member of acting agency with a paid seat | |
| member_membership = acting_agency.memberships.find_or_create_by!(user: path_b_member) do |m| | |
| m.role = "agent" | |
| m.status = "active" | |
| end | |
| member_membership.update!(status: "active") | |
| # Assign a paid seat in acting agency to the member | |
| acting_seat = acting_sub.subscription_seats.find_or_create_by!(agency_membership: member_membership) | |
| # Also make the member a member of the subagency owner's future subagency | |
| # (they need to exist in both agencies for transfer to work) | |
| # Create the parent agency invitation (Duck Travel invites acting agency) | |
| # Reset any prior invitation first | |
| ParentAgencyInvitation.where( | |
| agency: agency, | |
| email: path_b_owner.email | |
| ).where.not(status: "accepted").destroy_all | |
| invitation = ParentAgencyInvitation.find_by( | |
| agency: agency, | |
| email: path_b_owner.email, | |
| status: "pending" | |
| ) | |
| unless invitation | |
| seat = sub.subscription_seats.unoccupied.first | |
| raise "No unoccupied seats in #{agency.name} for the invitation" unless seat | |
| invitation = ParentAgencyInvitation.create!( | |
| agency: agency, | |
| acting_agency: acting_agency, | |
| email: path_b_owner.email, | |
| invited_by: owner, | |
| paid: true, | |
| status: "pending" | |
| ) | |
| seat.update!(parent_agency_invitation: invitation) | |
| end | |
| acceptance_token = invitation.to_sgid(purpose: :parent_agency_invitation).to_s | |
| invitation_url = "/parent_agency_invitations/#{acceptance_token}" | |
| # ---------------------------------------------------------------- | |
| # Print instructions | |
| # ---------------------------------------------------------------- | |
| puts <<~INSTRUCTIONS | |
| ============================================================ | |
| PATH A: Manual Seat Assignment | |
| ============================================================ | |
| This tests SubscriptionSeats::Assign — the fix that fires | |
| ReplaceIndividualWithAgencyJob when an owner manually assigns | |
| a paid seat to a member with an active personal subscription. | |
| Login: john@ducktravel.com / #{PASSWORD} | |
| Navigate: /agencies/#{agency.id}/team | |
| Steps: | |
| 1. Find "PathA Advisor" (eng1s712-path-a@example.com) in the table | |
| 2. Confirm "Seat paid by" column is empty (no paid seat) | |
| 3. Click the three-dot menu → "Assign paid seat" | |
| 4. Verify success flash and "Seat paid by" now shows the agency name | |
| 5. Check Sidekiq at /admin/sidekiq — ReplaceIndividualWithAgencyJob | |
| should have been enqueued with user_id=#{path_a_user.id} | |
| IDs: | |
| Agency: #{agency.id} (#{agency.name}) | |
| User: #{path_a_user.id} | |
| Membership: #{path_a_membership.id} | |
| Subscription: #{path_a_user.subscription.id} (stripe: #{path_a_user.subscription.stripe_subscription_id}) | |
| ============================================================ | |
| PATH B: Subagency Seat Transfer | |
| ============================================================ | |
| This tests ParentAgencyInvitations::Accept#transfer_memberships! | |
| — the fix that fires ReplaceIndividualWithAgencyJob for each | |
| transferred member who has an active personal subscription. | |
| Login: eng1s712-path-b-owner@example.com / #{PASSWORD} | |
| Navigate: #{invitation_url} | |
| Steps: | |
| 1. Log in as the subagency owner (PathB Owner) | |
| 2. Visit the invitation URL above | |
| 3. Choose "Use existing agency" and select "ENG1S-712 Acting Agency" | |
| 4. Submit the acceptance form | |
| 5. After redirect, check Sidekiq — ReplaceIndividualWithAgencyJob | |
| should have been enqueued with user_id=#{path_b_member.id} | |
| (the transferred member with the personal subscription) | |
| IDs: | |
| Host agency: #{agency.id} (#{agency.name}) | |
| Acting agency: #{acting_agency.id} (#{acting_agency.name}) | |
| Subagency owner: #{path_b_owner.id} | |
| Transferred member: #{path_b_member.id} | |
| Member subscription: #{path_b_member.subscription.id} (stripe: #{path_b_member.subscription.stripe_subscription_id}) | |
| Invitation: #{invitation.id} | |
| ============================================================ | |
| INSTRUCTIONS | |
| end | |
| desc "Reset ENG1S-712 test data" | |
| task reset: :environment do | |
| puts "Resetting ENG1S-712 test data..." | |
| emails = %w[ | |
| eng1s712-path-a@example.com | |
| eng1s712-path-b-owner@example.com | |
| eng1s712-path-b-member@example.com | |
| eng1s712-target@example.com | |
| ] | |
| emails.each do |email| | |
| user = User.find_by(email: email) | |
| next unless user | |
| user.subscription&.destroy | |
| user.agency_memberships.each do |m| | |
| m.subscription_seats.update_all(agency_membership_id: nil) | |
| m.destroy | |
| end | |
| end | |
| Agency.find_by(name: "ENG1S-712 Acting Agency")&.destroy | |
| ParentAgencyInvitation.where(email: "eng1s712-path-b-owner@example.com").destroy_all | |
| puts "Done." | |
| end | |
| end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment