@@ -8,6 +8,7 @@ import bubble.cloud.CloudServiceDriver; | |||||
import bubble.cloud.compute.ComputeNodeSizeType; | import bubble.cloud.compute.ComputeNodeSizeType; | ||||
import bubble.dao.account.message.AccountMessageDAO; | import bubble.dao.account.message.AccountMessageDAO; | ||||
import bubble.dao.app.*; | import bubble.dao.app.*; | ||||
import bubble.dao.bill.AccountPaymentArchivedDAO; | |||||
import bubble.dao.bill.BillDAO; | import bubble.dao.bill.BillDAO; | ||||
import bubble.dao.cloud.AnsibleRoleDAO; | import bubble.dao.cloud.AnsibleRoleDAO; | ||||
import bubble.dao.cloud.BubbleDomainDAO; | import bubble.dao.cloud.BubbleDomainDAO; | ||||
@@ -16,13 +17,13 @@ import bubble.dao.cloud.CloudServiceDAO; | |||||
import bubble.dao.device.DeviceDAO; | import bubble.dao.device.DeviceDAO; | ||||
import bubble.model.account.*; | import bubble.model.account.*; | ||||
import bubble.model.app.*; | import bubble.model.app.*; | ||||
import bubble.model.bill.Bill; | |||||
import bubble.model.bill.BubblePlan; | import bubble.model.bill.BubblePlan; | ||||
import bubble.model.cloud.*; | import bubble.model.cloud.*; | ||||
import bubble.server.BubbleConfiguration; | import bubble.server.BubbleConfiguration; | ||||
import bubble.service.SearchService; | import bubble.service.SearchService; | ||||
import bubble.service.boot.SelfNodeService; | import bubble.service.boot.SelfNodeService; | ||||
import lombok.Getter; | import lombok.Getter; | ||||
import lombok.NonNull; | |||||
import lombok.extern.slf4j.Slf4j; | import lombok.extern.slf4j.Slf4j; | ||||
import org.cobbzilla.util.cache.Refreshable; | import org.cobbzilla.util.cache.Refreshable; | ||||
import org.cobbzilla.wizard.dao.AbstractCRUDDAO; | 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.util.daemon.ZillaRuntime.daemon; | ||||
import static org.cobbzilla.wizard.model.IdentifiableBase.CTIME_ASC; | import static org.cobbzilla.wizard.model.IdentifiableBase.CTIME_ASC; | ||||
import static org.cobbzilla.wizard.resources.ResourceUtil.invalidEx; | import static org.cobbzilla.wizard.resources.ResourceUtil.invalidEx; | ||||
import static org.hibernate.criterion.Restrictions.isNotNull; | |||||
@Repository @Slf4j | @Repository @Slf4j | ||||
public class AccountDAO extends AbstractCRUDDAO<Account> implements SqlViewSearchableDAO<Account> { | public class AccountDAO extends AbstractCRUDDAO<Account> implements SqlViewSearchableDAO<Account> { | ||||
@@ -68,7 +70,6 @@ public class AccountDAO extends AbstractCRUDDAO<Account> implements SqlViewSearc | |||||
@Autowired private AccountMessageDAO messageDAO; | @Autowired private AccountMessageDAO messageDAO; | ||||
@Autowired private DeviceDAO deviceDAO; | @Autowired private DeviceDAO deviceDAO; | ||||
@Autowired private SelfNodeService selfNodeService; | @Autowired private SelfNodeService selfNodeService; | ||||
@Autowired private BillDAO billDAO; | |||||
@Autowired private SearchService searchService; | @Autowired private SearchService searchService; | ||||
@Autowired private ReferralCodeDAO referralCodeDAO; | @Autowired private ReferralCodeDAO referralCodeDAO; | ||||
@@ -311,51 +312,67 @@ public class AccountDAO extends AbstractCRUDDAO<Account> implements SqlViewSearc | |||||
log.info("copyTemplates completed: "+acct); | 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 | // 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 | // cannot delete account with unpaid bills | ||||
final List<Bill> unpaid = billDAO.findUnpaidByAccount(uuid); | |||||
final var billDAO = configuration.getBean(BillDAO.class); | |||||
final var unpaid = billDAO.findUnpaidByAccount(uuid); | |||||
if (!unpaid.isEmpty()) { | 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 | // for referral codes owned by us, set account to null, leave accountUuid in place | ||||
final List<ReferralCode> 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 | // 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)); | if (usedCode != null) referralCodeDAO.update(usedCode.setClaimedBy(null)); | ||||
// stash the deletion policy for later use, the policy object will be deleted in deleteDependencies | // 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); | configuration.deleteDependencies(account); | ||||
log.info("delete: finished deleting account-dependent objects"); | |||||
log.info("delete: finished deleting account-dependent objects - " + currentThread().getName()); | |||||
switch (deletionPolicy) { | switch (deletionPolicy) { | ||||
case full_delete: | case full_delete: | ||||
super.delete(uuid); | super.delete(uuid); | ||||
break; | |||||
case block_delete: default: | |||||
return; | |||||
default: | |||||
// includes case block_delete | |||||
update(account.setParent(null) | 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 | // once activated (any accounts exist), you can never go back | ||||
@@ -413,4 +430,7 @@ public class AccountDAO extends AbstractCRUDDAO<Account> implements SqlViewSearc | |||||
} | } | ||||
} | } | ||||
@NonNull public List<Account> findDeleted() { | |||||
return list(criteria().add(isNotNull("deleted"))); | |||||
} | |||||
} | } |
@@ -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<AccountPaymentArchived> | |||||
implements SqlViewSearchableDAO<AccountPaymentArchived> { | |||||
// 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)); | |||||
} | |||||
} |
@@ -108,7 +108,7 @@ public class Account extends IdentifiableBaseParentEntity implements TokenPrinci | |||||
@ECSearchable(filter=true) @ECField(index=10) | @ECSearchable(filter=true) @ECField(index=10) | ||||
@HasValue(message="err.name.required") | @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; | @Getter private String name; | ||||
public Account setName (String n) { this.name = n == null ? null : n.toLowerCase(); return this; } | public Account setName (String n) { this.name = n == null ? null : n.toLowerCase(); return this; } | ||||
public boolean hasName () { return !empty(name); } | public boolean hasName () { return !empty(name); } | ||||
@@ -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<Bill> 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<AccountPayment> 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<AccountPaymentMethod> paymentMethods) { | |||||
return setPaymentMethodsJson(json(paymentMethods, DB_JSON_MAPPER)); | |||||
} | |||||
} |
@@ -83,7 +83,8 @@ public class AccountPaymentMethod extends IdentifiableBase implements HasAccount | |||||
public static final String DEFAULT_MASKED_PAYMENT_INFO = "XXXX-".repeat(3)+"XXXX"; | public static final String DEFAULT_MASKED_PAYMENT_INFO = "XXXX-".repeat(3)+"XXXX"; | ||||
@ECSearchable @ECField(index=50, type=EntityFieldType.opaque_string) | @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; | @Getter @Setter private String maskedPaymentInfo = DEFAULT_MASKED_PAYMENT_INFO; | ||||
@ECSearchable @ECField(index=60) | @ECSearchable @ECField(index=60) | ||||
@@ -30,6 +30,8 @@ import static org.cobbzilla.wizard.model.entityconfig.annotations.ECForeignKeySe | |||||
}) | }) | ||||
public class Bill extends IdentifiableBase implements HasAccountNoName { | public class Bill extends IdentifiableBase implements HasAccountNoName { | ||||
public static final int PERIOD_FIELDS_MAX_LENGTH = 20; | |||||
@ECSearchable(fkDepth=shallow) @ECField(index=10) | @ECSearchable(fkDepth=shallow) @ECField(index=10) | ||||
@ECForeignKey(entity=Account.class) | @ECForeignKey(entity=Account.class) | ||||
@Column(nullable=false, updatable=false, length=UUID_MAXLEN) | @Column(nullable=false, updatable=false, length=UUID_MAXLEN) | ||||
@@ -53,15 +55,15 @@ public class Bill extends IdentifiableBase implements HasAccountNoName { | |||||
public boolean unpaid() { return !paid(); } | public boolean unpaid() { return !paid(); } | ||||
@ECSearchable @ECField(index=50, type=EntityFieldType.opaque_string) | @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; | @ECIndex @Getter @Setter private String periodLabel; | ||||
@ECSearchable @ECField(index=60, type=EntityFieldType.opaque_string) | @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; | @Getter @Setter private String periodStart; | ||||
@ECSearchable @ECField(index=70, type=EntityFieldType.opaque_string) | @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; | @Getter @Setter private String periodEnd; | ||||
public int daysInPeriod () { return BillPeriod.daysInPeriod(periodStart, periodEnd); } | public int daysInPeriod () { return BillPeriod.daysInPeriod(periodStart, periodEnd); } | ||||
@@ -41,6 +41,7 @@ import static org.cobbzilla.util.reflect.ReflectionUtil.copy; | |||||
@Entity @NoArgsConstructor @Accessors(chain=true) | @Entity @NoArgsConstructor @Accessors(chain=true) | ||||
public class BubblePlan extends IdentifiableBaseParentEntity implements HasAccount, HasPriority { | 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 int MAX_CHARGENAME_LEN = 12; | ||||
public static final String[] UPDATE_FIELDS = { | public static final String[] UPDATE_FIELDS = { | ||||
@@ -59,7 +60,7 @@ public class BubblePlan extends IdentifiableBaseParentEntity implements HasAccou | |||||
@ECSearchable(filter=true) @ECField(index=10) | @ECSearchable(filter=true) @ECField(index=10) | ||||
@HasValue(message="err.name.required") | @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; | @Getter @Setter private String name; | ||||
@ECSearchable @ECField(index=20) | @ECSearchable @ECField(index=20) | ||||
@@ -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); |
@@ -1,19 +1,76 @@ | |||||
/** | /** | ||||
* Copyright (c) 2020 Bubble, Inc. All rights reserved. | * 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; | 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 bubble.test.ActivatedBubbleModelTestBase; | ||||
import lombok.extern.slf4j.Slf4j; | |||||
import lombok.NonNull; | |||||
import org.junit.Before; | |||||
import org.junit.Test; | 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 { | public class AccountDeletionTest extends ActivatedBubbleModelTestBase { | ||||
@Override protected String getManifest() { return "manifest-test"; } | @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 testFullAccountDeletion() throws Exception { modelTest("account_deletion/full_delete_account"); } | ||||
@Test public void testBlockAccountDeletion() throws Exception { modelTest("account_deletion/block_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<String, Object> 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()); | |||||
} | |||||
} | } |
@@ -77,7 +77,7 @@ | |||||
"before": "sleep 1s", | "before": "sleep 1s", | ||||
"comment": "as root, check inbox for initial email verification message", | "comment": "as root, check inbox for initial email verification message", | ||||
"request": { | "request": { | ||||
"session": "rootSession", | |||||
"session": "<<rootSessionName>>", | |||||
"uri": "debug/inbox/email/{{user.policy.firstEmail}}?action=verify" | "uri": "debug/inbox/email/{{user.policy.firstEmail}}?action=verify" | ||||
}, | }, | ||||
"response": { | "response": { | ||||
@@ -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 | |||||
] |
@@ -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 | |||||
] |
@@ -147,8 +147,8 @@ | |||||
}, | }, | ||||
{ | { | ||||
"before": "sleep 15s", | |||||
"comment": "verify account plans, should be one, verify enabled", | "comment": "verify account plans, should be one, verify enabled", | ||||
"before": "sleep 15s", | |||||
"request": { "uri": "me/plans" }, | "request": { "uri": "me/plans" }, | ||||
"response": { | "response": { | ||||
"check": [ | "check": [ | ||||