Skip to content

Commit f5bc210

Browse files
committed
Scope billing run deletion filters to the current container
deleteBillingRuns() filtered the invoice, invoicedItems, and miscCharges tables by objectid/invoiceId alone, so a billing admin in one container could delete or detach billing data in any other container by passing foreign objectids. All filters are now container-scoped via SimpleFilter.createContainerFilter(). Adds an EHR_BillingManager.TestCase integration test, registered through EHR_BillingModule.getIntegrationTests(), that seeds a complete billing run in each of two folders and verifies that ids from another container are ignored by both the testOnly preview and the actual delete, while same-container deletion continues to remove the run and detach its misc charges.
1 parent 185d044 commit f5bc210

2 files changed

Lines changed: 150 additions & 5 deletions

File tree

ehr_billing/src/org/labkey/ehr_billing/EHR_BillingManager.java

Lines changed: 143 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@
1616

1717
package org.labkey.ehr_billing;
1818

19+
import org.junit.After;
20+
import org.junit.Assert;
21+
import org.junit.Before;
22+
import org.junit.Test;
1923
import org.labkey.api.collections.CaseInsensitiveHashMap;
2024
import org.labkey.api.data.CompareType;
2125
import org.labkey.api.data.Container;
@@ -36,14 +40,20 @@
3640
import org.labkey.api.query.QueryUpdateServiceException;
3741
import org.labkey.api.security.User;
3842
import org.labkey.api.data.RuntimeSQLException;
43+
import org.labkey.api.util.GUID;
44+
import org.labkey.api.util.JunitUtil;
45+
import org.labkey.api.util.TestContext;
3946

4047
import java.sql.SQLException;
4148
import java.util.ArrayList;
4249
import java.util.Arrays;
4350
import java.util.Collection;
4451
import java.util.Collections;
52+
import java.util.Date;
53+
import java.util.HashSet;
4554
import java.util.List;
4655
import java.util.Map;
56+
import java.util.Set;
4757

4858
public class EHR_BillingManager
4959
{
@@ -68,11 +78,11 @@ public List<String> deleteBillingRuns(User user, Container container, Collection
6878
TableInfo miscCharges = EHR_BillingSchema.getInstance().getSchema().getTable(EHR_BillingSchema.TABLE_MISC_CHARGES);
6979

7080
//create filters
71-
SimpleFilter objectIdFilter = new SimpleFilter(FieldKey.fromString("objectid"), pks, CompareType.IN);
72-
SimpleFilter invoiceIdFilter = new SimpleFilter(FieldKey.fromString("invoiceId"), pks, CompareType.IN);
73-
SimpleFilter invoiceRunIdFilter = new SimpleFilter(FieldKey.fromString("invoiceRunId"), pks, CompareType.IN);
81+
SimpleFilter objectIdFilter = createContainerScopedInFilter(container, "objectid", pks);
82+
SimpleFilter invoiceIdFilter = createContainerScopedInFilter(container, "invoiceId", pks);
83+
SimpleFilter invoiceRunIdFilter = createContainerScopedInFilter(container, "invoiceRunId", pks);
7484

75-
SimpleFilter miscChargesFilter = new SimpleFilter(FieldKey.fromString("invoiceId"), pks, CompareType.IN);
85+
SimpleFilter miscChargesFilter = createContainerScopedInFilter(container, "invoiceId", pks);
7686

7787
//perform the work
7888
List<String> ret = new ArrayList<>();
@@ -114,6 +124,11 @@ public List<String> deleteBillingRuns(User user, Container container, Collection
114124
return ret;
115125
}
116126

127+
private SimpleFilter createContainerScopedInFilter(Container container, String columnName, Collection<String> values)
128+
{
129+
return SimpleFilter.createContainerFilter(container).addInClause(FieldKey.fromString(columnName), values);
130+
}
131+
117132
private void deleteInvoiceRuns(TableInfo tableInfo, Map<String, Object>[] rows, User user, Container container) throws QueryUpdateServiceException, BatchValidationException, InvalidKeyException
118133
{
119134
if(rows.length>0)
@@ -141,4 +156,127 @@ public Container getBillingContainer(Container c)
141156

142157
}
143158

144-
}
159+
public static class TestCase extends Assert
160+
{
161+
private static final String FOLDER_A = "EHRBillingDeleteTestA";
162+
private static final String FOLDER_B = "EHRBillingDeleteTestB";
163+
164+
private User _user;
165+
private Container _containerA;
166+
private Container _containerB;
167+
private String _runIdA;
168+
private String _runIdB;
169+
170+
@Before
171+
public void setUp()
172+
{
173+
_user = TestContext.get().getUser();
174+
deleteTestFolders();
175+
176+
Container junit = JunitUtil.getTestContainer();
177+
_containerA = createBillingFolder(junit, FOLDER_A);
178+
_containerB = createBillingFolder(junit, FOLDER_B);
179+
180+
_runIdA = insertBillingRun(_containerA);
181+
_runIdB = insertBillingRun(_containerB);
182+
}
183+
184+
@After
185+
public void tearDown()
186+
{
187+
deleteTestFolders();
188+
}
189+
190+
@Test
191+
public void testDeleteBillingRunsIsContainerScoped() throws Exception
192+
{
193+
EHR_BillingManager manager = EHR_BillingManager.get();
194+
EHR_BillingSchema schema = EHR_BillingSchema.getInstance();
195+
196+
// A testOnly preview issued from container A targeting container B's run must not see container B's rows
197+
for (String summary : manager.deleteBillingRuns(_user, _containerA, List.of(_runIdB), true))
198+
assertTrue("Preview from another container should count 0 rows, but got: " + summary, summary.startsWith("0 "));
199+
200+
// An actual delete issued from container A targeting container B's run must leave container B untouched
201+
manager.deleteBillingRuns(_user, _containerA, List.of(_runIdB), false);
202+
assertEquals("invoiceRuns row in container B should survive a delete issued from container A", 1, containerRowCount(schema.getTableInvoiceRuns(), _containerB));
203+
assertEquals("invoice row in container B should survive a delete issued from container A", 1, containerRowCount(schema.getInvoice(), _containerB));
204+
assertEquals("invoicedItems row in container B should survive a delete issued from container A", 1, containerRowCount(schema.getTableInvoiceItems(), _containerB));
205+
assertEquals("miscCharges row in container B should still reference its invoice", 1, miscChargesWithInvoiceCount(_containerB));
206+
207+
// Positive control: deleting a run from its own container removes its rows
208+
manager.deleteBillingRuns(_user, _containerA, List.of(_runIdA), false);
209+
assertEquals("invoiceRuns row in container A should be deleted", 0, containerRowCount(schema.getTableInvoiceRuns(), _containerA));
210+
assertEquals("invoice row in container A should be deleted", 0, containerRowCount(schema.getInvoice(), _containerA));
211+
assertEquals("invoicedItems row in container A should be deleted", 0, containerRowCount(schema.getTableInvoiceItems(), _containerA));
212+
assertEquals("miscCharges row in container A should be detached from the deleted invoice", 0, miscChargesWithInvoiceCount(_containerA));
213+
assertEquals("miscCharges row in container A should not be deleted", 1, containerRowCount(schema.getMiscCharges(), _containerA));
214+
}
215+
216+
private Container createBillingFolder(Container parent, String name)
217+
{
218+
Container c = ContainerManager.createContainer(parent, name, _user);
219+
Set<Module> active = new HashSet<>(c.getActiveModules());
220+
active.add(ModuleLoader.getInstance().getModule(EHR_BillingModule.NAME));
221+
c.setActiveModules(active, _user);
222+
return c;
223+
}
224+
225+
private String insertBillingRun(Container c)
226+
{
227+
EHR_BillingSchema schema = EHR_BillingSchema.getInstance();
228+
String runId = GUID.makeGUID();
229+
String invoiceNumber = c.getName();
230+
231+
Map<String, Object> run = new CaseInsensitiveHashMap<>();
232+
run.put("objectid", runId);
233+
run.put("runDate", new Date());
234+
run.put("container", c.getId());
235+
Table.insert(_user, schema.getTableInvoiceRuns(), run);
236+
237+
Map<String, Object> invoice = new CaseInsensitiveHashMap<>();
238+
invoice.put("invoiceNumber", invoiceNumber);
239+
invoice.put("invoiceRunId", runId);
240+
invoice.put("container", c.getId());
241+
Table.insert(_user, schema.getInvoice(), invoice);
242+
243+
Map<String, Object> invoicedItem = new CaseInsensitiveHashMap<>();
244+
invoicedItem.put("objectId", GUID.makeGUID());
245+
invoicedItem.put("invoiceId", runId);
246+
invoicedItem.put("invoiceNumber", invoiceNumber);
247+
invoicedItem.put("container", c.getId());
248+
Table.insert(_user, schema.getTableInvoiceItems(), invoicedItem);
249+
250+
Map<String, Object> miscCharge = new CaseInsensitiveHashMap<>();
251+
miscCharge.put("objectid", GUID.makeGUID());
252+
miscCharge.put("invoiceId", runId);
253+
miscCharge.put("container", c.getId());
254+
Table.insert(_user, schema.getMiscCharges(), miscCharge);
255+
256+
return runId;
257+
}
258+
259+
private long containerRowCount(TableInfo table, Container c)
260+
{
261+
return new TableSelector(table, SimpleFilter.createContainerFilter(c), null).getRowCount();
262+
}
263+
264+
private long miscChargesWithInvoiceCount(Container c)
265+
{
266+
SimpleFilter filter = SimpleFilter.createContainerFilter(c);
267+
filter.addCondition(FieldKey.fromParts("invoiceId"), null, CompareType.NONBLANK);
268+
return new TableSelector(EHR_BillingSchema.getInstance().getMiscCharges(), filter, null).getRowCount();
269+
}
270+
271+
private void deleteTestFolders()
272+
{
273+
Container junit = JunitUtil.getTestContainer();
274+
for (String name : List.of(FOLDER_A, FOLDER_B))
275+
{
276+
Container c = junit.getChild(name);
277+
if (c != null)
278+
ContainerManager.delete(c, _user);
279+
}
280+
}
281+
}
282+
}

ehr_billing/src/org/labkey/ehr_billing/EHR_BillingModule.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,13 @@ public Set<String> getSchemaNames()
127127
return Collections.singleton(EHR_BillingSchema.NAME);
128128
}
129129

130+
@Override
131+
@NotNull
132+
public Set<Class> getIntegrationTests()
133+
{
134+
return Collections.singleton(EHR_BillingManager.TestCase.class);
135+
}
136+
130137
@NotNull
131138
@Override
132139
public JSONObject getPageContextJson(ContainerUser ctx)

0 commit comments

Comments
 (0)