diff --git a/bubble-server/src/main/java/bubble/dao/account/AccountDAO.java b/bubble-server/src/main/java/bubble/dao/account/AccountDAO.java index aa437583..f5f48fda 100644 --- a/bubble-server/src/main/java/bubble/dao/account/AccountDAO.java +++ b/bubble-server/src/main/java/bubble/dao/account/AccountDAO.java @@ -8,6 +8,7 @@ import bubble.cloud.CloudServiceDriver; import bubble.cloud.compute.ComputeNodeSizeType; import bubble.dao.account.message.AccountMessageDAO; import bubble.dao.app.*; +import bubble.dao.bill.AccountPaymentArchivedDAO; import bubble.dao.bill.BillDAO; import bubble.dao.cloud.AnsibleRoleDAO; import bubble.dao.cloud.BubbleDomainDAO; @@ -16,13 +17,13 @@ import bubble.dao.cloud.CloudServiceDAO; import bubble.dao.device.DeviceDAO; import bubble.model.account.*; import bubble.model.app.*; -import bubble.model.bill.Bill; import bubble.model.bill.BubblePlan; import bubble.model.cloud.*; import bubble.server.BubbleConfiguration; import bubble.service.SearchService; import bubble.service.boot.SelfNodeService; import lombok.Getter; +import lombok.NonNull; import lombok.extern.slf4j.Slf4j; import org.cobbzilla.util.cache.Refreshable; import org.cobbzilla.wizard.dao.AbstractCRUDDAO; @@ -48,6 +49,7 @@ import static java.util.concurrent.TimeUnit.MINUTES; import static org.cobbzilla.util.daemon.ZillaRuntime.daemon; import static org.cobbzilla.wizard.model.IdentifiableBase.CTIME_ASC; import static org.cobbzilla.wizard.resources.ResourceUtil.invalidEx; +import static org.hibernate.criterion.Restrictions.isNotNull; @Repository @Slf4j public class AccountDAO extends AbstractCRUDDAO implements SqlViewSearchableDAO { @@ -68,7 +70,6 @@ public class AccountDAO extends AbstractCRUDDAO implements SqlViewSearc @Autowired private AccountMessageDAO messageDAO; @Autowired private DeviceDAO deviceDAO; @Autowired private SelfNodeService selfNodeService; - @Autowired private BillDAO billDAO; @Autowired private SearchService searchService; @Autowired private ReferralCodeDAO referralCodeDAO; @@ -311,51 +312,67 @@ public class AccountDAO extends AbstractCRUDDAO implements SqlViewSearc log.info("copyTemplates completed: "+acct); } - @Override public void delete(String uuid) { - final Account account = findByUuid(uuid); - + @Override public void delete(@NonNull final String uuid) { // you cannot delete the account that owns the current network - if (account.getUuid().equals(configuration.getThisNetwork().getAccount())) { - throw invalidEx("err.delete.invalid", "cannot delete account ("+account.getUuid()+") that owns current network ("+configuration.getThisNetwork().getUuid()+")", account.getUuid()); + if (uuid.equals(configuration.getThisNetwork().getAccount())) { + throw invalidEx("err.delete.invalid", + "cannot delete account that owns current network: " + + uuid + " - " + configuration.getThisNetwork().getUuid(), + uuid); } + deleteTransactional(uuid); + searchService.flushCache(this); + } + + @Transactional(Transactional.TxType.REQUIRES_NEW) + private void deleteTransactional(@NonNull final String uuid) { + // loading, and actually checking if the account with given UUID exists + final var account = findByUuid(uuid); + // cannot delete account with unpaid bills - final List unpaid = billDAO.findUnpaidByAccount(uuid); + final var billDAO = configuration.getBean(BillDAO.class); + final var unpaid = billDAO.findUnpaidByAccount(uuid); if (!unpaid.isEmpty()) { - throw invalidEx("err.delete.unpaidBills", "cannot delete account ("+account.getUuid()+") with "+unpaid.size()+" unpaid bills", account.getUuid()); + throw invalidEx("err.delete.unpaidBills", + "cannot delete account with unpaid bills: " + uuid + " - " + unpaid.size(), + uuid); } // for referral codes owned by us, set account to null, leave accountUuid in place - final List ownedCodes = referralCodeDAO.findByAccount(uuid); - for (ReferralCode c : ownedCodes) referralCodeDAO.update(c.setAccount(null)); + final var ownedCodes = referralCodeDAO.findByAccount(uuid); + for (var c : ownedCodes) referralCodeDAO.update(c.setAccount(null)); // for referral a code we used, set usedBy to null, leave usedByUuid in place - final ReferralCode usedCode = referralCodeDAO.findCodeUsedBy(uuid); + final var usedCode = referralCodeDAO.findCodeUsedBy(uuid); if (usedCode != null) referralCodeDAO.update(usedCode.setClaimedBy(null)); // stash the deletion policy for later use, the policy object will be deleted in deleteDependencies - final AccountDeletionPolicy deletionPolicy = policyDAO.findSingleByAccount(uuid).getDeletionPolicy(); + final var deletionPolicy = policyDAO.findSingleByAccount(uuid).getDeletionPolicy(); + + // archive all payment data for the account just on the first deletion request: + configuration.getBean(AccountPaymentArchivedDAO.class).createForAccount(account); - log.info("delete ("+currentThread().getName()+"): starting to delete account-dependent objects"); + log.info("delete: starting to delete account-dependent objects - " + currentThread().getName()); configuration.deleteDependencies(account); - log.info("delete: finished deleting account-dependent objects"); + log.info("delete: finished deleting account-dependent objects - " + currentThread().getName()); switch (deletionPolicy) { case full_delete: super.delete(uuid); - break; - case block_delete: default: + return; + default: + // includes case block_delete update(account.setParent(null) - .setAdmin(null) - .setSuspended(null) - .setDescription(null) - .setDeleted() - .setUrl(null) - .setAutoUpdatePolicy(EMPTY_AUTO_UPDATE_POLICY) - .setHashedPassword(HashedPassword.DELETED)); - break; + .setAdmin(null) + .setSuspended(null) + .setDescription(null) + .setDeleted() + .setUrl(null) + .setAutoUpdatePolicy(EMPTY_AUTO_UPDATE_POLICY) + .setHashedPassword(HashedPassword.DELETED)); + return; } - searchService.flushCache(this); } // once activated (any accounts exist), you can never go back @@ -413,4 +430,7 @@ public class AccountDAO extends AbstractCRUDDAO implements SqlViewSearc } } + @NonNull public List findDeleted() { + return list(criteria().add(isNotNull("deleted"))); + } } diff --git a/bubble-server/src/main/java/bubble/dao/bill/AccountPaymentArchivedDAO.java b/bubble-server/src/main/java/bubble/dao/bill/AccountPaymentArchivedDAO.java new file mode 100644 index 00000000..bd23a173 --- /dev/null +++ b/bubble-server/src/main/java/bubble/dao/bill/AccountPaymentArchivedDAO.java @@ -0,0 +1,55 @@ +/** + * Copyright (c) 2020 Bubble, Inc. All rights reserved. + * For personal (non-commercial) use, see license: https://getbubblenow.com/bubble-license/ + */ +package bubble.dao.bill; + +import bubble.model.account.Account; +import bubble.model.bill.AccountPaymentArchived; +import lombok.NonNull; +import org.cobbzilla.wizard.dao.AbstractCRUDDAO; +import org.cobbzilla.wizard.dao.SqlViewSearchableDAO; +import org.hibernate.criterion.Order; +import org.springframework.stereotype.Repository; + +import static org.cobbzilla.util.daemon.ZillaRuntime.die; + +@Repository +public class AccountPaymentArchivedDAO + extends AbstractCRUDDAO + implements SqlViewSearchableDAO { + + // newest first + @Override public Order getDefaultSortOrder() { return ORDER_CTIME_DESC; } + + public AccountPaymentArchived findByAccountUuid(@NonNull final String accountUuid) { + return findByUniqueField("accountUuid", accountUuid); + } + + @NonNull public AccountPaymentArchived createForAccount(@NonNull final Account account) { + final var allBills = getConfiguration().getBean(BillDAO.class).findByAccount(account.getUuid()); + final var allPayments = getConfiguration().getBean(AccountPaymentDAO.class).findByAccount(account.getUuid()); + final var allMethods = getConfiguration().getBean(AccountPaymentMethodDAO.class) + .findByAccount(account.getUuid()); + + // Payment info should be present and archived only for currently non deleted account. So, the first deletion + // request will archive those. Any call after that for already deleted account that has some payment info is + // strange and is most probably result of an error - deleted account should not be able to create any payment + // records. + if (account.deleted()) { + if (allBills.size() + allPayments.size() + allMethods.size() > 0) { + return die("Payment records present for already deleted account " + account.getUuid()); + // Stopping further execution to avoid loss of data. Check these payment entries and then manually + // decide what to do with those. Call delete again after these are cleared from database. + } + // else, just return already existing entry. Note that any deleted account should have an entry here, while + // such entries might have empty arrays for bills, payment and payment methods. + return findByAccountUuid(account.getUuid()); + } + // Finally, create new entry here only if this is the first deletion call for the specified account: + return create(new AccountPaymentArchived().setAccountUuid(account.getUuid()) + .setBills(allBills) + .setPayments(allPayments) + .setPaymentMethods(allMethods)); + } +} diff --git a/bubble-server/src/main/java/bubble/model/account/Account.java b/bubble-server/src/main/java/bubble/model/account/Account.java index 22510141..d4fcf57e 100644 --- a/bubble-server/src/main/java/bubble/model/account/Account.java +++ b/bubble-server/src/main/java/bubble/model/account/Account.java @@ -108,7 +108,7 @@ public class Account extends IdentifiableBaseParentEntity implements TokenPrinci @ECSearchable(filter=true) @ECField(index=10) @HasValue(message="err.name.required") - @ECIndex(unique=true) @Column(nullable=false, updatable=false, length=100) + @ECIndex(unique=true) @Column(nullable=false, updatable=false, length=NAME_MAX_LENGTH) @Getter private String name; public Account setName (String n) { this.name = n == null ? null : n.toLowerCase(); return this; } public boolean hasName () { return !empty(name); } diff --git a/bubble-server/src/main/java/bubble/model/bill/AccountPaymentArchived.java b/bubble-server/src/main/java/bubble/model/bill/AccountPaymentArchived.java new file mode 100644 index 00000000..99f8edb2 --- /dev/null +++ b/bubble-server/src/main/java/bubble/model/bill/AccountPaymentArchived.java @@ -0,0 +1,64 @@ +/** + * Copyright (c) 2020 Bubble, Inc. All rights reserved. + * For personal (non-commercial) use, see license: https://getbubblenow.com/bubble-license/ + */ +package bubble.model.bill; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.Accessors; +import org.cobbzilla.wizard.model.IdentifiableBase; +import org.cobbzilla.wizard.model.entityconfig.EntityFieldType; +import org.cobbzilla.wizard.model.entityconfig.annotations.*; +import org.hibernate.annotations.Type; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Transient; +import java.util.List; + +import static bubble.ApiConstants.DB_JSON_MAPPER; +import static org.cobbzilla.util.json.JsonUtil.json; +import static org.cobbzilla.wizard.model.crypto.EncryptedTypes.ENCRYPTED_STRING; + +@ECType(root=true) @ECTypeCreate(method="DISABLED") +@ECTypeURIs(listFields={"accountUuid", "ctime"}) +@Entity @Accessors(chain=true) @NoArgsConstructor +public class AccountPaymentArchived extends IdentifiableBase { + + @ECSearchable @ECField(index=10, type=EntityFieldType.opaque_string) + @ECIndex(unique=true) @Column(unique=true, updatable=false, length=UUID_MAXLEN) + @Getter @Setter private String accountUuid; + + @ECSearchable @ECField(index=20, type=EntityFieldType.opaque_string) + @Type(type=ENCRYPTED_STRING) + @Column(updatable=false, nullable=false, columnDefinition="varchar") // no length limit + @JsonIgnore @Getter @Setter private String billsJson; + + @Transient public Bill[] getBills() { return json(billsJson, Bill[].class); } + public AccountPaymentArchived setBills(List bills) { return setBillsJson(json(bills, DB_JSON_MAPPER)); } + + @ECSearchable @ECField(index=30) + @Type(type=ENCRYPTED_STRING) + @Column(updatable=false, nullable=false, columnDefinition="varchar") // no length limit + @JsonIgnore @Getter @Setter private String paymentsJson; + + @Transient public AccountPayment[] getPayments() { return json(paymentsJson, AccountPayment[].class); } + public AccountPaymentArchived setPayments(List payments) { + return setPaymentsJson(json(payments, DB_JSON_MAPPER)); + } + + @ECSearchable @ECField(index=40) + @Type(type=ENCRYPTED_STRING) + @Column(updatable=false, nullable=false, columnDefinition="varchar") // no length limit + @JsonIgnore @Getter @Setter private String paymentMethodsJson; + + @Transient public AccountPaymentMethod[] getPaymentMethods() { + return json(paymentMethodsJson, AccountPaymentMethod[].class); + } + public AccountPaymentArchived setPaymentMethods(List paymentMethods) { + return setPaymentMethodsJson(json(paymentMethods, DB_JSON_MAPPER)); + } +} diff --git a/bubble-server/src/main/java/bubble/model/bill/AccountPaymentMethod.java b/bubble-server/src/main/java/bubble/model/bill/AccountPaymentMethod.java index 8d09d15e..ff9b2e13 100644 --- a/bubble-server/src/main/java/bubble/model/bill/AccountPaymentMethod.java +++ b/bubble-server/src/main/java/bubble/model/bill/AccountPaymentMethod.java @@ -83,7 +83,8 @@ public class AccountPaymentMethod extends IdentifiableBase implements HasAccount public static final String DEFAULT_MASKED_PAYMENT_INFO = "XXXX-".repeat(3)+"XXXX"; @ECSearchable @ECField(index=50, type=EntityFieldType.opaque_string) - @Type(type=ENCRYPTED_STRING) @Column(updatable=false, columnDefinition="varchar("+(100+ENC_PAD)+") NOT NULL") + @Type(type=ENCRYPTED_STRING) + @Column(updatable=false, columnDefinition="varchar(" + (NAME_MAXLEN + ENC_PAD) + ") NOT NULL") @Getter @Setter private String maskedPaymentInfo = DEFAULT_MASKED_PAYMENT_INFO; @ECSearchable @ECField(index=60) diff --git a/bubble-server/src/main/java/bubble/model/bill/Bill.java b/bubble-server/src/main/java/bubble/model/bill/Bill.java index 7031422c..cb434f9d 100644 --- a/bubble-server/src/main/java/bubble/model/bill/Bill.java +++ b/bubble-server/src/main/java/bubble/model/bill/Bill.java @@ -30,6 +30,8 @@ import static org.cobbzilla.wizard.model.entityconfig.annotations.ECForeignKeySe }) public class Bill extends IdentifiableBase implements HasAccountNoName { + public static final int PERIOD_FIELDS_MAX_LENGTH = 20; + @ECSearchable(fkDepth=shallow) @ECField(index=10) @ECForeignKey(entity=Account.class) @Column(nullable=false, updatable=false, length=UUID_MAXLEN) @@ -53,15 +55,15 @@ public class Bill extends IdentifiableBase implements HasAccountNoName { public boolean unpaid() { return !paid(); } @ECSearchable @ECField(index=50, type=EntityFieldType.opaque_string) - @Column(nullable=false, updatable=false, length=20) + @Column(nullable=false, updatable=false, length=PERIOD_FIELDS_MAX_LENGTH) @ECIndex @Getter @Setter private String periodLabel; @ECSearchable @ECField(index=60, type=EntityFieldType.opaque_string) - @Column(nullable=false, updatable=false, length=20) + @Column(nullable=false, updatable=false, length=PERIOD_FIELDS_MAX_LENGTH) @Getter @Setter private String periodStart; @ECSearchable @ECField(index=70, type=EntityFieldType.opaque_string) - @Column(nullable=false, updatable=false, length=20) + @Column(nullable=false, updatable=false, length=PERIOD_FIELDS_MAX_LENGTH) @Getter @Setter private String periodEnd; public int daysInPeriod () { return BillPeriod.daysInPeriod(periodStart, periodEnd); } diff --git a/bubble-server/src/main/java/bubble/model/bill/BubblePlan.java b/bubble-server/src/main/java/bubble/model/bill/BubblePlan.java index 0832180d..b7f4e85b 100644 --- a/bubble-server/src/main/java/bubble/model/bill/BubblePlan.java +++ b/bubble-server/src/main/java/bubble/model/bill/BubblePlan.java @@ -41,6 +41,7 @@ import static org.cobbzilla.util.reflect.ReflectionUtil.copy; @Entity @NoArgsConstructor @Accessors(chain=true) public class BubblePlan extends IdentifiableBaseParentEntity implements HasAccount, HasPriority { + public static final int PLAN_NAME_MAX_LENGTH = 200; public static final int MAX_CHARGENAME_LEN = 12; public static final String[] UPDATE_FIELDS = { @@ -59,7 +60,7 @@ public class BubblePlan extends IdentifiableBaseParentEntity implements HasAccou @ECSearchable(filter=true) @ECField(index=10) @HasValue(message="err.name.required") - @ECIndex(unique=true) @Column(nullable=false, updatable=false, length=200) + @ECIndex(unique=true) @Column(nullable=false, updatable=false, length=PLAN_NAME_MAX_LENGTH) @Getter @Setter private String name; @ECSearchable @ECField(index=20) diff --git a/bubble-server/src/main/resources/db/migration/V2020042301__add_account_payment_archived.sql b/bubble-server/src/main/resources/db/migration/V2020042301__add_account_payment_archived.sql new file mode 100644 index 00000000..05ece37b --- /dev/null +++ b/bubble-server/src/main/resources/db/migration/V2020042301__add_account_payment_archived.sql @@ -0,0 +1,16 @@ +-- Copyright (c) 2020 Bubble, Inc. All rights reserved. +-- For personal (non-commercial) use, see license: https://getbubblenow.com/bubble-license/ + +CREATE TABLE public.account_payment_archived ( + uuid character varying(100) NOT NULL, + ctime bigint NOT NULL, + mtime bigint NOT NULL, + account_uuid character varying(100), + bills_json character varying NOT NULL, + payment_methods_json character varying NOT NULL, + payments_json character varying NOT NULL +); +ALTER TABLE account_payment_archived OWNER TO bubble; +ALTER TABLE account_payment_archived ADD CONSTRAINT account_payment_archived_pkey PRIMARY KEY (uuid); +ALTER TABLE account_payment_archived ADD CONSTRAINT account_payment_archived_uk_account UNIQUE (account_uuid); +CREATE UNIQUE INDEX account_payment_archived_uniq_account_uuid ON account_payment_archived USING btree (account_uuid); diff --git a/bubble-server/src/test/java/bubble/test/system/AccountDeletionTest.java b/bubble-server/src/test/java/bubble/test/system/AccountDeletionTest.java index f389155a..b3d4e1a7 100644 --- a/bubble-server/src/test/java/bubble/test/system/AccountDeletionTest.java +++ b/bubble-server/src/test/java/bubble/test/system/AccountDeletionTest.java @@ -1,19 +1,76 @@ /** * Copyright (c) 2020 Bubble, Inc. All rights reserved. - * For personal (non-commercial) use, see license: https://bubblev.com/bubble-license/ + * For personal (non-commercial) use, see license: https://getbubblenow.com/bubble-license/ */ package bubble.test.system; +import bubble.dao.account.AccountDAO; +import bubble.dao.bill.AccountPaymentArchivedDAO; +import bubble.model.account.Account; +import bubble.model.bill.AccountPayment; +import bubble.model.bill.AccountPaymentMethod; +import bubble.model.bill.Bill; import bubble.test.ActivatedBubbleModelTestBase; -import lombok.extern.slf4j.Slf4j; +import lombok.NonNull; +import org.junit.Before; import org.junit.Test; -@Slf4j +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + public class AccountDeletionTest extends ActivatedBubbleModelTestBase { @Override protected String getManifest() { return "manifest-test"; } + @Before public void truncatePaymentArchive() { + final var archivedInfoDAO = getBean(AccountPaymentArchivedDAO.class); + archivedInfoDAO.delete(archivedInfoDAO.findAll()); + } + @Test public void testFullAccountDeletion() throws Exception { modelTest("account_deletion/full_delete_account"); } @Test public void testBlockAccountDeletion() throws Exception { modelTest("account_deletion/block_delete_account"); } + @Test public void testBlockDeleteAccountWithPayments() throws Exception { + checkArchivedPayments(modelTest("account_deletion/block_delete_account_with_payments")); + } + + @Test public void testFullDeleteAccountWithPayments() throws Exception { + checkArchivedPayments(modelTest("account_deletion/full_delete_account_with_payments")); + } + + private void checkArchivedPayments(@NonNull final Map modelTestCtx) { + final var accountDAO = getBean(AccountDAO.class); + final var archivedInfoDAO = getBean(AccountPaymentArchivedDAO.class); + + final var deletedAccounts = accountDAO.findDeleted(); + // the account was fully deleted at the end of the JSON test + assertEquals("Wrong number of deleted accounts found", 0, deletedAccounts.size()); + final var deletedAccount = (Account) modelTestCtx.get("testAccount"); + + // there should be just 1 archived payment info records corresponding to that 1 deleted account + assertEquals("Archived payments record not created for deleted user", 1, archivedInfoDAO.countAll().intValue()); + + final var archivedInfo = archivedInfoDAO.findByAccountUuid(deletedAccount.getUuid()); + assertNotNull("Archived payment info not found for deleted user", archivedInfo); + + final var archivedBills = archivedInfo.getBills(); + assertEquals("Only 1 bill should be in for deleted account", 1, archivedBills.length); + assertEquals("Wrong bill archived", ((Bill[]) modelTestCtx.get("bills"))[0], archivedBills[0]); + + final var archivedPayments = archivedInfo.getPayments(); + assertEquals("Only 1 payment should be in for deleted account", 1, archivedPayments.length); + assertEquals("Wrong payment archived", + ((AccountPayment[]) modelTestCtx.get("payments"))[0], archivedPayments[0]); + assertEquals("Archived payment should be for archived bill", + archivedBills[0].getUuid(), archivedPayments[0].getBill()); + + final var archivedPaymentMethods = archivedInfo.getPaymentMethods(); + assertEquals("Only 1 payment method should be in for deleted account", 1, archivedPaymentMethods.length); + assertEquals("Wrong payment method archived", + ((AccountPaymentMethod[]) modelTestCtx.get("paymentMethods"))[0], archivedPaymentMethods[0]); + assertEquals("Archived payment method should be for used within archived payment", + archivedPayments[0].getPaymentMethod(), archivedPaymentMethods[0].getUuid()); + } } diff --git a/bubble-server/src/test/resources/models/include/new_account.json b/bubble-server/src/test/resources/models/include/new_account.json index ba736863..1f6fc0f3 100644 --- a/bubble-server/src/test/resources/models/include/new_account.json +++ b/bubble-server/src/test/resources/models/include/new_account.json @@ -77,7 +77,7 @@ "before": "sleep 1s", "comment": "as root, check inbox for initial email verification message", "request": { - "session": "rootSession", + "session": "<>", "uri": "debug/inbox/email/{{user.policy.firstEmail}}?action=verify" }, "response": { diff --git a/bubble-server/src/test/resources/models/tests/account_deletion/block_delete_account_with_payments.json b/bubble-server/src/test/resources/models/tests/account_deletion/block_delete_account_with_payments.json new file mode 100644 index 00000000..8b18ea15 --- /dev/null +++ b/bubble-server/src/test/resources/models/tests/account_deletion/block_delete_account_with_payments.json @@ -0,0 +1,147 @@ +[ + { + "comment": "create a user account to block delete later on", + "request": { + "uri": "users", + "method": "put", + "entity": { + "name": "user_with_payment_to_block_delete", + "password": "password1!", + "agreeToTerms": true, + "contact": { "type": "email", "info": "user_with_payment_to_block_delete@example.com" } + } + }, + "response": { "store": "testAccount" } + }, + + { + "comment": "login as that new user", + "request": { + "session": "new", + "uri": "auth/login", + "entity": { "name": "{{ testAccount.name }}", "password": "password1!" } + }, + "response": { "store": "testAccount", "sessionName": "userSession", "session": "token" } + }, + + { + "comment": "get plans", + "request": { "uri": "plans" }, + "response": { "store": "plans", "check": [{ "condition": "len(json) >= 1" }] } + }, + + { + "comment": "add plan, using 'free' payment method", + "request": { + "uri": "me/plans", + "method": "put", + "entity": { + "name": "test-net-{{rand 5}}", + "domain": "{{defaultDomain}}", + "locale": "en_US", + "timezone": "EST", + "plan": "{{plans.[0].name}}", + "footprint": "US", + "paymentMethodObject": { "paymentMethodType": "free", "paymentInfo": "free" } + } + }, + "response": { "store": "accountPlan" } + }, + + { + "comment": "as root, verify bill exists and is paid", + "before": "sleep 15s", + "request": { "session": "rootSession", "uri": "users/{{testAccount.uuid}}/bills" }, + "response": { + "store": "bills", + "check": [ + { "condition": "len(json) === 1" }, + { "condition": "json[0].getPlan() === plans[0].getUuid()" }, + { "condition": "json[0].getAccountPlan() === accountPlan.getUuid()" }, + { "condition": "json[0].getTotal() === plans[0].getPrice()" }, + { "condition": "json[0].getStatus().name() === 'paid'" } + ] + } + }, + + { + "comment": "verify payment exists and is successful", + "request": { "uri": "users/{{testAccount.uuid}}/payments" }, + "response": { + "store": "payments", + "check": [ + { "condition": "len(json) === 1" }, + { "condition": "json[0].getPlan() === plans[0].getUuid()" }, + { "condition": "json[0].getAccountPlan() === accountPlan.getUuid()" }, + { "condition": "json[0].getAmount() === plans[0].getPrice()" }, + { "condition": "json[0].getStatus().name() === 'success'" }, + { "condition": "json[0].getBill() === bills[0].getUuid()" } + ] + } + }, + + { + "comment": "verify account payment methods, should be one", + "request": { "uri": "users/{{testAccount.uuid}}/paymentMethods" }, + "response": { + "store": "paymentMethods", + "check": [ + { "condition": "len(json) === 1" }, + { "condition": "json[0].getPaymentMethodType().name() === 'free'" }, + { "condition": "json[0].getMaskedPaymentInfo() === 'XXXXXXXX'" }, + { "condition": "json[0].getUuid() === payments[0].getPaymentMethod()" } + ] + } + }, + + { + "comment": "now (block) delete the account", + "request": { + "uri": "users/{{testAccount.uuid}}", + "method": "delete" + } + }, + + { + "comment": "lookup user, expect that it is still there, just marked as deleted", + "request": { "uri": "users/{{testAccount.uuid}}" }, + "response": { + "check": [ + { "condition": "json.getUuid() === testAccount.getUuid()" }, + { "condition": "json.getName() === testAccount.getName()" }, + { "condition": "json.deleted()" } + ] + } + }, + + { + "comment": "look up for deleted account's bills - none", + "request": { "uri": "users/{{testAccount.uuid}}/bills" }, + "response": { "check": [{ "condition": "len(json) === 0" }] } + }, + + { + "comment": "look up for deleted account's payments - none", + "request": { "uri": "users/{{testAccount.uuid}}/payments" }, + "response": { "check": [{ "condition": "len(json) === 0" }] } + }, + + { + "comment": "look up for deleted account's payment methods - none", + "request": { "uri": "users/{{testAccount.uuid}}/paymentMethods" }, + "response": { "check": [{ "condition": "len(json) === 0" }] } + }, + + { + "comment": "try deleting the same account again - expect fully deletion this time even without policy", + "request": { "uri": "users/{{testAccount.uuid}}", "method": "delete" } + }, + + { + "comment": "lookup user, expect there's no such user now", + "request": { "uri": "users/{{testAccount.uuid}}" }, + "response": { "status": 404 } + } + + // test continues within Java's JUnit test as there are not resource methods implemented for archived payment data +] diff --git a/bubble-server/src/test/resources/models/tests/account_deletion/full_delete_account_with_payments.json b/bubble-server/src/test/resources/models/tests/account_deletion/full_delete_account_with_payments.json new file mode 100644 index 00000000..6f05d8c1 --- /dev/null +++ b/bubble-server/src/test/resources/models/tests/account_deletion/full_delete_account_with_payments.json @@ -0,0 +1,129 @@ +[ + { + "comment": "create a user account", + "request": { + "uri": "users", + "method": "put", + "entity": { + "name": "user_with_payment_to_delete", + "password": "password1!", + "agreeToTerms": true, + "contact": { "type": "email", "info": "user_with_payment_to_delete@example.com" } + } + }, + "response": { "store": "testAccount" } + }, + + { + "comment": "login as new user", + "request": { + "session": "new", + "uri": "auth/login", + "entity": { "name": "{{ testAccount.name }}", "password": "password1!" } + }, + "response": { + "store": "testAccount", + "sessionName": "userSession", + "session": "token" + } + }, + + { + "comment": "get plans", + "request": { "uri": "plans" }, + "response": { "store": "plans", "check": [{ "condition": "len(json) >= 1" }] } + }, + + { + "comment": "add plan, using 'free' payment method", + "request": { + "uri": "me/plans", + "method": "put", + "entity": { + "name": "test-net-{{rand 5}}", + "domain": "{{defaultDomain}}", + "locale": "en_US", + "timezone": "EST", + "plan": "{{plans.[0].name}}", + "footprint": "US", + "paymentMethodObject": { "paymentMethodType": "free", "paymentInfo": "free" } + } + }, + "response": { "store": "accountPlan" } + }, + + { + "comment": "as root, verify bill exists and is paid", + "before": "sleep 15s", + "request": { "session": "rootSession", "uri": "users/{{testAccount.uuid}}/bills" }, + "response": { + "store": "bills", + "check": [ + { "condition": "len(json) === 1" }, + { "condition": "json[0].getPlan() === plans[0].getUuid()" }, + { "condition": "json[0].getAccountPlan() === accountPlan.getUuid()" }, + { "condition": "json[0].getTotal() === plans[0].getPrice()" }, + { "condition": "json[0].getStatus().name() === 'paid'" } + ] + } + }, + + { + "comment": "verify payment exists and is successful", + "request": { "uri": "users/{{testAccount.uuid}}/payments" }, + "response": { + "store": "payments", + "check": [ + { "condition": "len(json) === 1" }, + { "condition": "json[0].getPlan() === plans[0].getUuid()" }, + { "condition": "json[0].getAccountPlan() === accountPlan.getUuid()" }, + { "condition": "json[0].getAmount() === plans[0].getPrice()" }, + { "condition": "json[0].getStatus().name() === 'success'" }, + { "condition": "json[0].getBill() === bills[0].getUuid()" } + ] + } + }, + + { + "comment": "verify account payment methods, should be one", + "request": { "uri": "users/{{testAccount.uuid}}/paymentMethods" }, + "response": { + "store": "paymentMethods", + "check": [ + { "condition": "len(json) === 1" }, + { "condition": "json[0].getPaymentMethodType().name() === 'free'" }, + { "condition": "json[0].getMaskedPaymentInfo() === 'XXXXXXXX'" }, + { "condition": "json[0].getUuid() === payments[0].getPaymentMethod()" } + ] + } + }, + + { + "comment": "look up that account's policy", + "request": { "uri": "users/{{testAccount.uuid}}/policy" }, + "response": { "store": "policy", "check": [{ "condition": "len(json.getAccountContacts()) == 1" }] } + }, + + { + "comment": "set deletion policy to full_delete for that account", + "request": { + "uri": "users/{{testAccount.uuid}}/policy", + "data": "policy", + "entity": { "deletionPolicy": "full_delete" } + }, + "response": { "store": "policy", "check": [{ "condition": "json.getDeletionPolicy().name() == 'full_delete'" }] } + }, + + { + "comment": "now (full) delete the account", + "request": { "uri": "users/{{testAccount.uuid}}", "method": "delete" } + }, + + { + "comment": "lookup user, expect there's no such user now", + "request": { "uri": "users/{{testAccount.uuid}}" }, + "response": { "status": 404 } + } + + // test continues within Java's JUnit test as there are not resource methods implemented for archived payment data +] diff --git a/bubble-server/src/test/resources/models/tests/payment/pay_free.json b/bubble-server/src/test/resources/models/tests/payment/pay_free.json index 9328bfb4..72436093 100644 --- a/bubble-server/src/test/resources/models/tests/payment/pay_free.json +++ b/bubble-server/src/test/resources/models/tests/payment/pay_free.json @@ -147,8 +147,8 @@ }, { - "before": "sleep 15s", "comment": "verify account plans, should be one, verify enabled", + "before": "sleep 15s", "request": { "uri": "me/plans" }, "response": { "check": [