Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

1571: Import stores backend #1606

Merged
merged 14 commits into from
Sep 24, 2024
Merged

1571: Import stores backend #1606

merged 14 commits into from
Sep 24, 2024

Conversation

f1sh1918
Copy link
Contributor

@f1sh1918 f1sh1918 commented Aug 28, 2024

Short description

As a user i want to import stores accepting stores via csv. The backend part is implemented here

Proposed changes

  • create reusable functions for mapping and storing the accepting store data in the database
  • add a dryRun function to check how many stores are affected, before applying db actions
  • add missing react component tests
  • add missing tests for backend functions
  • TODO integration test for the mutation (Add test for importCsvStores #1605), since i couldn't get mocking bearer token for and have to investigate more

Side effects

Testing

  1. Create a Project Store Manager and login
  2. Go to "Akzeptanzpartner verwalten"
  3. Add a csv, data can be found here (1475: Csv store import module #1572)
  4. Import the stores and check the database tables
  5. Open native nuernberg app and check if the stores and the correct data is available
  6. Do the same using "testlauf" flag with a different csv and check that the database was not affected

Resolved issues

Fixes: #1571

@@ -8,9 +8,6 @@ import bayernConfig from '../../project-configs/bayern/config'
import CreateCardsButtonBar from './CreateCardsButtonBar'

jest.useFakeTimers()
jest.mock('csv-stringify/browser/esm/sync', () => ({
stringify: jest.fn(),
}))
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moved this to the jest.setup for all tests using it

@f1sh1918 f1sh1918 force-pushed the 1571-import-stores-backend branch from 31424f6 to 05e9a3e Compare August 28, 2024 14:35
@f1sh1918 f1sh1918 force-pushed the 1571-import-stores-backend branch from 05e9a3e to cf835cb Compare August 28, 2024 14:49
/** Returns null if string can't be trimmed f.e. empty string
* Removes subsequent whitespaces
* */
fun String?.clean(removeSubsequentWhitespaces: Boolean = true): String? {
Copy link
Contributor

@seluianova seluianova Sep 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit-picky:
if we call this function with removeSubsequentWhitespaces = false, we get the same effect as trim().
so I'm not quite sure why this extra branch of logic is needed?
when we need only trim() why not just call trim() ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i adjusted this function to return null for empty string and added removeSubsequentStrings false if it is used for the csv import via administration

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was it on purpose to remove replaceNa()?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you give me an explanation about what that notes should be that will be replaced with this function. i couldn't find any good example for that.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i readded replaceNa() just in case to not break anything.
It would be good to add an example in the tests @steffenkleinle. But this can be done is a separate task
I couldn't find much information about that and how a string with such a note looks like

@seluianova
Copy link
Contributor

One more finding:
If some fields are null, they are inserted as empty in the database.
For example, the first 2 records (with empty emails) were inserted from CSV and the rest were inserted the old way:
image
I assume null-values are expected in such cases.


fun mapCsvToStore(csvStore: CSVAcceptingStore): AcceptingStore {
return AcceptingStore(
csvStore.name.clean()!!, COUNTRY_CODE, csvStore.location.clean()!!, csvStore.postalCode.clean()!!, csvStore.street.clean()!!, csvStore.houseNumber.clean()!!, "", csvStore.longitude!!.toDouble(), csvStore.latitude!!.toDouble(), csvStore.categoryId!!.toInt(), csvStore.email.clean()!!, csvStore.telephone.clean()!!, csvStore.homepage.clean()!!, csvStore.discountDE.orEmpty() + "\n\n" + csvStore.discountEN.orEmpty(), null, null
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if discountDE and discountEN are null, I think the expected description should be null, or?
currently it's "\n\n"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes i added a check

Copy link
Contributor

@seluianova seluianova Sep 13, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

now if discountDE is null and discountEN is not null ("test description", for example), it's saved like "\n\ntest description".
probably it should be "test description"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

addressed this in last commit by using a mutableList now its even better extendable if we want to add another language for discount someday

Copy link
Member

@ztefanie ztefanie left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tested normal store import via run config. Works as expected

Is it expected that project admins cannot create users with the role store import? Because only we want to use this feature? If not, users need the option to select the PROJECT_STORE_MANAGER role, when creating a new user.

Add a new file to the other csv resources as an example of the csv import please.

View is disabled for project with disabled upload and visible for nuernberg as expected.

import worked as expected.

@@ -29,6 +31,13 @@ const StoresImportController = (): ReactElement => {
return <StoresImport fields={storeManagement.fields} />
}

const CenteredSpinner = styled(Spinner)`
z-index: 999;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggest to use 500 so we could possible put something before in the future.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but what should be before an loading spinner f.e ? even its shown over a modal it should be highest index

@f1sh1918
Copy link
Contributor Author

f1sh1918 commented Sep 9, 2024

Tested normal store import via run config. Works as expected

Is it expected that project admins cannot create users with the role store import? Because only we want to use this feature? If not, users need the option to select the PROJECT_STORE_MANAGER role, when creating a new user.

Add a new file to the other csv resources as an example of the csv import please.

View is disabled for project with disabled upload and visible for nuernberg as expected.

import worked as expected.

yes for the moment the store import is an internal function and not provided to customers. When we decide to sell that function, we have to add the possibility to create users with this role

@f1sh1918 f1sh1918 force-pushed the 1571-import-stores-backend branch from 34403ec to e1f49cc Compare September 10, 2024 19:43
Copy link
Contributor

@seluianova seluianova left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maximale Dateigröße: 2MB

If I try to upload 2Mb file (it's appx 5500 lines), I'm getting an error on the backend side:

[JettyServerThreadPool-33] WARN io.javalin.Javalin - Body greater than max size (1000000 bytes)
io.javalin.http.HttpResponseException: Content Too Large
	at io.javalin.http.servlet.MaxRequestSize.throwContentTooLargeIfContentTooLarge(JavalinServletContext.kt:236)
	at io.javalin.http.Context.bodyAsBytes(Context.kt:147)
	at io.javalin.http.servlet.JavalinServletContext.access$bodyAsBytes$s-1678783089(JavalinServletContext.kt:65)
	at io.javalin.http.servlet.JavalinServletContext$body$2.invoke(JavalinServletContext.kt:133)
	at io.javalin.http.servlet.JavalinServletContext$body$2.invoke(JavalinServletContext.kt:133)
	at kotlin.SynchronizedLazyImpl.getValue(LazyJVM.kt:74)
	at io.javalin.http.servlet.JavalinServletContext.getBody(JavalinServletContext.kt:133)
	at io.javalin.http.servlet.JavalinServletContext.bodyAsBytes(JavalinServletContext.kt:134)
	at io.javalin.http.Context.body(Context.kt:138)
	at io.javalin.plugin.bundled.DevLoggingPlugin.httpDevLogger(DevLoggerPlugin.kt:55)
	at io.javalin.plugin.bundled.DevLoggingPlugin.onInitialize$lambda$0(DevLoggerPlugin.kt:29)
	at io.javalin.http.servlet.JavalinServlet.writeResponseAndLog(JavalinServlet.kt:114)
	at io.javalin.http.servlet.JavalinServlet.handleSync(JavalinServlet.kt:68)
	at io.javalin.http.servlet.JavalinServlet.handle(JavalinServlet.kt:50)
	at io.javalin.http.servlet.JavalinServlet.service(JavalinServlet.kt:30)
	at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:587)
	at io.javalin.jetty.JavalinJettyServlet.service(JavalinJettyServlet.kt:52)
	at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:587)
	at org.eclipse.jetty.servlet.ServletHolder.handle(ServletHolder.java:764)
	at org.eclipse.jetty.servlet.ServletHandler.doHandle(ServletHandler.java:529)
	at org.eclipse.jetty.server.handler.ScopedHandler.nextHandle(ScopedHandler.java:221)
	at org.eclipse.jetty.server.session.SessionHandler.doHandle(SessionHandler.java:1580)
	at org.eclipse.jetty.server.handler.ScopedHandler.nextHandle(ScopedHandler.java:221)
	at org.eclipse.jetty.server.handler.ContextHandler.doHandle(ContextHandler.java:1381)
	at org.eclipse.jetty.server.handler.ScopedHandler.nextScope(ScopedHandler.java:176)
	at org.eclipse.jetty.servlet.ServletHandler.doScope(ServletHandler.java:484)
	at org.eclipse.jetty.server.session.SessionHandler.doScope(SessionHandler.java:1553)
	at org.eclipse.jetty.server.handler.ScopedHandler.nextScope(ScopedHandler.java:174)
	at org.eclipse.jetty.server.handler.ContextHandler.doScope(ContextHandler.java:1303)
	at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:129)
	at org.eclipse.jetty.server.handler.StatisticsHandler.handle(StatisticsHandler.java:173)
	at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:122)
	at org.eclipse.jetty.server.Server.handle(Server.java:563)
	at org.eclipse.jetty.server.HttpChannel$RequestDispatchable.dispatch(HttpChannel.java:1598)
	at org.eclipse.jetty.server.HttpChannel.dispatch(HttpChannel.java:753)
	at org.eclipse.jetty.server.HttpChannel.handle(HttpChannel.java:501)
	at org.eclipse.jetty.server.HttpConnection.onFillable(HttpConnection.java:287)
	at org.eclipse.jetty.io.AbstractConnection$ReadCallback.succeeded(AbstractConnection.java:314)
	at org.eclipse.jetty.io.FillInterest.fillable(FillInterest.java:100)
	at org.eclipse.jetty.io.SelectableChannelEndPoint$1.run(SelectableChannelEndPoint.java:53)
	at org.eclipse.jetty.util.thread.strategy.AdaptiveExecutionStrategy.runTask(AdaptiveExecutionStrategy.java:421)
	at org.eclipse.jetty.util.thread.strategy.AdaptiveExecutionStrategy.consumeTask(AdaptiveExecutionStrategy.java:390)
	at org.eclipse.jetty.util.thread.strategy.AdaptiveExecutionStrategy.tryProduce(AdaptiveExecutionStrategy.java:277)
	at org.eclipse.jetty.util.thread.strategy.AdaptiveExecutionStrategy.run(AdaptiveExecutionStrategy.java:199)
	at org.eclipse.jetty.util.thread.ReservedThreadExecutor$ReservedThread.run(ReservedThreadExecutor.java:411)
	at org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:969)
	at org.eclipse.jetty.util.thread.QueuedThreadPool$Runner.doRunJob(QueuedThreadPool.java:1194)
	at org.eclipse.jetty.util.thread.QueuedThreadPool$Runner.run(QueuedThreadPool.java:1149)
	at java.base/java.lang.Thread.run(Thread.java:840)

@f1sh1918
Copy link
Contributor Author

f1sh1918 commented Sep 13, 2024

Maximale Dateigröße: 2MB

If I try to upload 2Mb file (it's appx 5500 lines), I'm getting an error on the backend side:

[JettyServerThreadPool-33] WARN io.javalin.Javalin - Body greater than max size (1000000 bytes)
io.javalin.http.HttpResponseException: Content Too Large
	at io.javalin.http.servlet.MaxRequestSize.throwContentTooLargeIfContentTooLarge(JavalinServletContext.kt:236)
	at io.javalin.http.Context.bodyAsBytes(Context.kt:147)
	at io.javalin.http.servlet.JavalinServletContext.access$bodyAsBytes$s-1678783089(JavalinServletContext.kt:65)
	at io.javalin.http.servlet.JavalinServletContext$body$2.invoke(JavalinServletContext.kt:133)
	at io.javalin.http.servlet.JavalinServletContext$body$2.invoke(JavalinServletContext.kt:133)
	at kotlin.SynchronizedLazyImpl.getValue(LazyJVM.kt:74)
	at io.javalin.http.servlet.JavalinServletContext.getBody(JavalinServletContext.kt:133)
	at io.javalin.http.servlet.JavalinServletContext.bodyAsBytes(JavalinServletContext.kt:134)
	at io.javalin.http.Context.body(Context.kt:138)
	at io.javalin.plugin.bundled.DevLoggingPlugin.httpDevLogger(DevLoggerPlugin.kt:55)
	at io.javalin.plugin.bundled.DevLoggingPlugin.onInitialize$lambda$0(DevLoggerPlugin.kt:29)
	at io.javalin.http.servlet.JavalinServlet.writeResponseAndLog(JavalinServlet.kt:114)
	at io.javalin.http.servlet.JavalinServlet.handleSync(JavalinServlet.kt:68)
	at io.javalin.http.servlet.JavalinServlet.handle(JavalinServlet.kt:50)
	at io.javalin.http.servlet.JavalinServlet.service(JavalinServlet.kt:30)
	at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:587)
	at io.javalin.jetty.JavalinJettyServlet.service(JavalinJettyServlet.kt:52)
	at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:587)
	at org.eclipse.jetty.servlet.ServletHolder.handle(ServletHolder.java:764)
	at org.eclipse.jetty.servlet.ServletHandler.doHandle(ServletHandler.java:529)
	at org.eclipse.jetty.server.handler.ScopedHandler.nextHandle(ScopedHandler.java:221)
	at org.eclipse.jetty.server.session.SessionHandler.doHandle(SessionHandler.java:1580)
	at org.eclipse.jetty.server.handler.ScopedHandler.nextHandle(ScopedHandler.java:221)
	at org.eclipse.jetty.server.handler.ContextHandler.doHandle(ContextHandler.java:1381)
	at org.eclipse.jetty.server.handler.ScopedHandler.nextScope(ScopedHandler.java:176)
	at org.eclipse.jetty.servlet.ServletHandler.doScope(ServletHandler.java:484)
	at org.eclipse.jetty.server.session.SessionHandler.doScope(SessionHandler.java:1553)
	at org.eclipse.jetty.server.handler.ScopedHandler.nextScope(ScopedHandler.java:174)
	at org.eclipse.jetty.server.handler.ContextHandler.doScope(ContextHandler.java:1303)
	at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:129)
	at org.eclipse.jetty.server.handler.StatisticsHandler.handle(StatisticsHandler.java:173)
	at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:122)
	at org.eclipse.jetty.server.Server.handle(Server.java:563)
	at org.eclipse.jetty.server.HttpChannel$RequestDispatchable.dispatch(HttpChannel.java:1598)
	at org.eclipse.jetty.server.HttpChannel.dispatch(HttpChannel.java:753)
	at org.eclipse.jetty.server.HttpChannel.handle(HttpChannel.java:501)
	at org.eclipse.jetty.server.HttpConnection.onFillable(HttpConnection.java:287)
	at org.eclipse.jetty.io.AbstractConnection$ReadCallback.succeeded(AbstractConnection.java:314)
	at org.eclipse.jetty.io.FillInterest.fillable(FillInterest.java:100)
	at org.eclipse.jetty.io.SelectableChannelEndPoint$1.run(SelectableChannelEndPoint.java:53)
	at org.eclipse.jetty.util.thread.strategy.AdaptiveExecutionStrategy.runTask(AdaptiveExecutionStrategy.java:421)
	at org.eclipse.jetty.util.thread.strategy.AdaptiveExecutionStrategy.consumeTask(AdaptiveExecutionStrategy.java:390)
	at org.eclipse.jetty.util.thread.strategy.AdaptiveExecutionStrategy.tryProduce(AdaptiveExecutionStrategy.java:277)
	at org.eclipse.jetty.util.thread.strategy.AdaptiveExecutionStrategy.run(AdaptiveExecutionStrategy.java:199)
	at org.eclipse.jetty.util.thread.ReservedThreadExecutor$ReservedThread.run(ReservedThreadExecutor.java:411)
	at org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:969)
	at org.eclipse.jetty.util.thread.QueuedThreadPool$Runner.doRunJob(QueuedThreadPool.java:1194)
	at org.eclipse.jetty.util.thread.QueuedThreadPool$Runner.run(QueuedThreadPool.java:1149)
	at java.base/java.lang.Thread.run(Thread.java:840)

@seluianova
OK thx for testing.
i realized that a file with 2MB has even a larger content size (HTTP Request)
Example:
image
To ensure that nothing breaks i set the value to 5MB in the backend. But i have no clue how to calculate this on the basis of a 2MB csv file :/
But now the upload for large files work.
It even should now be sufficient for the user import i guess. But we may ask in the meeting with koblenz and create example files

@f1sh1918 f1sh1918 requested a review from seluianova September 13, 2024 19:30
@f1sh1918 f1sh1918 force-pushed the 1571-import-stores-backend branch from 0f55eaa to e515ae2 Compare September 14, 2024 08:13
@f1sh1918 f1sh1918 requested a review from seluianova September 14, 2024 08:15
@f1sh1918
Copy link
Contributor Author

f1sh1918 commented Sep 14, 2024

Uploading 10000 stores take 80seconds. I added a aprox. duration to the alert dialog, for mass uploads to inform the user that it may take a while

Copy link
Member

@steffenkleinle steffenkleinle left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tested on firefox, did not find any issues! Great work 🎉

Is it on purpose that this does not disappear again on its own?
image

/** Returns null if string can't be trimmed f.e. empty string
* Removes subsequent whitespaces
* */
fun String?.clean(removeSubsequentWhitespaces: Boolean = true): String? {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was it on purpose to remove replaceNa()?

@f1sh1918
Copy link
Contributor Author

Tested on firefox, did not find any issues! Great work 🎉

Is it on purpose that this does not disappear again on its own?
image

Yes

@seluianova
Copy link
Contributor

seluianova commented Sep 17, 2024

I would do a final review once you have addressed Steffen's comments if you don't mind, in order to re-test all together, but I think it should be good to go.

discounts.removeIf { it.isNullOrEmpty() }
return discounts.joinToString("\n\n")
}
fun mapCsvToStore(csvStore: CSVAcceptingStore): AcceptingStore {
Copy link
Contributor

@seluianova seluianova Sep 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

optional:
do we want to handle an error, when csvStore failed to be mapped into AcceptingStore?
it can happen for example if categoryId can't be parsed to Integer, or coordinates cannot be parsed to Double.
(or if some fields are missed)
currently the service returns 500 internal server error in such case.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can add an improvement github issue. I think this pr is now big enough :)

@f1sh1918 f1sh1918 force-pushed the 1571-import-stores-backend branch from 5a270ed to 1246274 Compare September 23, 2024 13:40
@f1sh1918
Copy link
Contributor Author

I would do a final review once you have addressed Steffen's comments if you don't mind, in order to re-test all together, but I think it should be good to go.

i think i addressed all import ones @seluianova

@seluianova
Copy link
Contributor

seluianova commented Sep 23, 2024

nit-picky thought:
currently we have a field configuration in administration, where the list of fields can be customized.
but, in the backend the list of fields is fixed anyway.
for example, if I remove discountEN from the configuration, I still can't upload stores without that field because the import into the backend will fail.

and vice versa:
in the frontend we have set some fields as mandatory, but in the backend they are not mandatory.
this means I can send an importAcceptingStores request directly to the backend (since this is technically possible) and upload stores with null-name, for example.

I'm not pushing to change this behavior right now, just highlighting it for discussion

I think at best this behavior between the front and back should be unified

@f1sh1918
Copy link
Contributor Author

f1sh1918 commented Sep 23, 2024

nit-picky thought: currently we have a field configuration in administration, where the list of fields can be customized. but, in the backend the list of fields is fixed anyway. for example, if I remove discountEN from the configuration, I still can't upload stores without that field because the import into the backend will fail.

and vice versa: in the frontend we have set some fields as mandatory, but in the backend they are not mandatory. this means I can send an importAcceptingStores request directly to the backend (since this is technically possible) and upload stores with null-name, for example.

I'm not pushing to change this behavior right now, just highlighting it for discussion

I think at best this behavior between the front and back should be unified

Absolutely valid input. I think this should be added in the cleanup.
I think the current issue is that we have two ways (1. the old via csv file and backend cron import job, that will be deleted, 2. Upload via gui)
If we ran the first accepting store import on production we can delete the old import and improve CSVAcceptingStore data class
#1591

Please feel free to add also other improvements or cleanups to that issue

Copy link
Contributor

@seluianova seluianova left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍
thanks for the great effort!

I think this should be added in the cleanup

not sure if validation on the backend is related to the clean up though, but maybe I'm missing something.
I think it could just be added to the mutation and it wouldn't affect the old import.
I see it like a separate issue - add the same validation in the backend as on the frontend.
I can create a ticket for that if it sounds reasonable to you.

@f1sh1918
Copy link
Contributor Author

👍 thanks for the great effort!

I think this should be added in the cleanup

not sure if validation on the backend is related to the clean up though, but maybe I'm missing something. I think it could just be added to the mutation and it wouldn't affect the old import. I see it like a separate issue - add the same validation in the backend as on the frontend. I can create a ticket for that if it sounds reasonable to you.

ah okay now i got it. Yes then please create a separate task

@f1sh1918 f1sh1918 enabled auto-merge September 24, 2024 08:02
@f1sh1918 f1sh1918 merged commit 5bd4139 into main Sep 24, 2024
1 check passed
@f1sh1918 f1sh1918 deleted the 1571-import-stores-backend branch September 24, 2024 08:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Acceptance Stores - Import acceptance stores backend
4 participants